`;
+
+ //call api and update the listener numbber
+ var xhr = new XMLHttpRequest();
+
+ function reqListener() {
+ var listenerNum = JSON.parse(this.response)[0].listeners.total;
+ listenerMounter(listenerNum);
+ setTimeout(callMaker, 20000);
+ }
+
+ function listenerMounter(number) {
+ var d = document.getElementById("listeners-num");
+ d.textContent = number + " coders listening right now";
+ }
+
+ function callMaker() {
+ xhr.addEventListener("load", reqListener);
+ xhr.open("GET", "/app/api/nowplaying");
+
+ xhr.send();
+ }
+
+ callMaker();
+}
\ No newline at end of file
diff --git a/codeRadio.js b/codeRadio.js
new file mode 100644
index 0000000..ff6666d
--- /dev/null
+++ b/codeRadio.js
@@ -0,0 +1,32 @@
+window.mobilecheck = function () {
+ var check = false;
+ (function (a) {
+ if (
+ /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(
+ a
+ ) ||
+ /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
+ a.substr(0, 4)
+ )
+ )
+ check = true;
+ })(navigator.userAgent || navigator.vendor || window.opera);
+ return check;
+};
+
+function showMenu() {
+ toggleClass("toggle-button-nav", "reverse-toggle-color");
+ toggleClass("nav", "show-main-nav-items");
+ toggleClass("site-nav", "expand-nav");
+}
+
+function toggleClass(id, className) {
+ var d = document.getElementById(id);
+ d.classList.toggle(className);
+}
+
+if (window && window.addEventListener) {
+ window.addEventListener("load", function () {
+ document.getElementById("toggle-button-nav").onclick = showMenu;
+ });
+}
\ No newline at end of file
diff --git a/index.html b/index.html
index ca0d1d2..a70c34f 100644
--- a/index.html
+++ b/index.html
@@ -1,13 +1,65 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- Code Radio
-
+ freeCodeCamp.org Code Radio
-
-
-
-
-
-
-
-
+
+
+
diff --git a/interface.css b/interface.css
index 939e353..03c10b8 100644
--- a/interface.css
+++ b/interface.css
@@ -14,7 +14,7 @@ body {
overflow: hidden;
}
-footer.footer {
+footer {
background-color: #0a0a23;
height: 70px;
width: 100%;
@@ -50,8 +50,15 @@ footer.footer {
top: 18px;
font-size: 20px;
}
+
#metaDisplay {
width: calc(100% - 210px);
+ opacity: 0;
+ transition: opacity 0.5s ease-out;
+}
+
+#metaDisplay.shown {
+ opacity: 1;
}
#playContainer {
@@ -59,6 +66,32 @@ footer.footer {
width: 70px;
padding: 15px;
cursor: pointer;
+ position: relative;
+}
+
+#playContainer::before {
+ content:"Push Play or Space Bar to Start Music";
+ font-size: 21px;
+ padding: 10px 100px 10px 20px;
+ font-weight: bold;
+ box-sizing: border-box;
+ position: absolute;
+ height: 100px;
+ width: 250px;
+ top: -110px;
+ left: -170px;
+ background-color: #1b1b32;
+ border-radius: 5px;
+ opacity: 0;
+ transition: opacity 0.5s ease-out;
+ background-image: url(https://cdn-media-1.freecodecamp.org/code-radio/cta.png);
+ background-position: 150px center;
+ background-repeat: no-repeat;
+ pointer-events: none;
+}
+
+#playContainer.cta::before {
+ opacity: 1;
}
#playContainer:hover {
@@ -147,7 +180,7 @@ footer.footer {
}
.animation {
- background-image: url("https://cdn-media-1.freecodecamp.org/code-radio/bg.png");
+ background-image: url("https://cdn-media-1.freecodecamp.org/code-radio/bg.jpg");
background-position: left bottom;
background-repeat: no-repeat;
position: relative;
@@ -155,7 +188,7 @@ footer.footer {
height: calc(100% - 158px);
}
-.animation::before {
+.animation.saron::before {
content: "";
background-image: url("https://cdn-media-1.freecodecamp.org/code-radio/Saron3.gif");
left: 388px;
@@ -165,10 +198,9 @@ footer.footer {
position: absolute;
}
-.details {
- display: none;
+details {
position: absolute;
- bottom: 50px;
+ bottom: 100px;
right: 50px;
background-color: rgba(10, 10, 35, 0.6);
color: #fff;
@@ -177,8 +209,8 @@ footer.footer {
transition: opacity 0.25s ease-out;
}
-.details:hover,
-.details[open] {
+details:hover,
+details[open] {
opacity: 1;
}
@@ -310,6 +342,11 @@ dd {
height: auto;
}
+nav select {
+ position: relative;
+ top: 15px;
+}
+
@media (max-width: 500px) {
.site-header {
padding-right: 0;
@@ -456,10 +493,6 @@ body {
flex-direction: column;
height: 200px;
}
- #container.animation,
- #visualizer {
- display: none;
- }
footer.footer {
display: none;
diff --git a/player.js b/player.js
index 4ee9d66..8cf2eba 100644
--- a/player.js
+++ b/player.js
@@ -1,15 +1,27 @@
-class CodeRadio {
+const _events = Symbol('events'),
+ _url = Symbol('url'),
+ _player = Symbol('player'),
+ _alternateMounts = Symbol('alternateMounts'),
+ _currentSong = Symbol('currentSong'),
+ _songStartedAt = Symbol('songStartedAt'),
+ _songDuration = Symbol('songDuration'),
+ _progressInterval = Symbol('progressInterval'),
+ _listeners = Symbol('listeners'),
+ _audioConfig = Symbol('audioConfig'),
+ _fastConnection = Symbol('fastConnection');
+
+export class CodeRadio {
+
constructor() {
/***
* General configuration options
*/
this.config = {
- baseColour: "rgb(10, 10, 35)",
- translucent: "rgba(10, 10, 35, 0.6)",
- multiplier: 0.7529,
metadataTimer: 2000
};
+ this[_fastConnection] = (!!navigator.connection) ? (navigator.connection.downlink > 1.5) : false;
+
/***
* The equalizer data is held as a seperate data set
* to allow for easy implementation of visualizers.
@@ -27,7 +39,7 @@ class CodeRadio {
// Some basic configuration for nicer audio transitions
// (Used in earlier projects and just maintained)
- this.audioConfig = {
+ this[_audioConfig] = {
targetVolume: 0,
maxVolume: 0.5,
volumeSteps: 0.1,
@@ -39,33 +51,25 @@ class CodeRadio {
* to it being a single audio element, there should be
* no memory leaks of extra floating audio elements.
*/
- this._url = "";
- this._player = new Audio();
- this._player.volume = this.audioConfig.maxVolume;
- this._player.crossOrigin = "anonymous";
+ this[_url] = "";
+ this[_alternateMounts] = [];
+ this[_player] = new Audio();
+ this[_player].volume = this[_audioConfig].maxVolume;
+ this[_player].crossOrigin = "anonymous";
// Note: the crossOrigin is needed to fix a CORS JavaScript requirement
/***
* There are a few *private* variables used
*/
- this._currentSong = {};
- this._songStartedAt = 0;
- this._songDuration = 0;
- this._progressInterval = false;
+ this[_currentSong] = {};
+ this[_songStartedAt] = 0;
+ this[_songDuration] = 0;
+ this[_progressInterval] = false;
+ this[_listeners] = 0;
this.getNowPlaying();
- this.meta = {
- container: document.getElementById("nowPlaying"),
- picture: document.querySelector('[data-meta="picture"]'),
- title: document.querySelector('[data-meta="title"]'),
- artist: document.querySelector('[data-meta="artist"]'),
- album: document.querySelector('[data-meta="album"]'),
- duration: document.querySelector('[data-meta="duration"]'),
- listeners: document.querySelector('[data-meta="listeners"]')
- };
-
- this.setupEventListeners();
+ document.addEventListener("keydown", evt => this.keyboardControl(evt));
}
/***
@@ -74,15 +78,37 @@ class CodeRadio {
* resets the URL.
*/
set url(url = false) {
- if (url && this._url === "") {
- this._url = url;
- this._player.src = url;
- this._player.play();
+ if (url) {
+ let playing = this.playing;
+ if (playing) this.pause();
+ this[_url] = url;
+ this[_player].src = url;
+ if (playing) this.play();
}
}
get url() {
- return this._url;
+ return this[_url];
+ }
+
+ get player() {
+ return this[_player];
+ }
+
+ set player(v) {
+ throw new Error('You cannot set the value of a readonly attribute');
+ }
+
+ set mounts(mounts) {
+ throw new Error('You cannot set the value of a readonly attribute');
+ }
+
+ set mount(mount) {
+ this.url = mount;
+ }
+
+ get mounts() {
+ return this[_alternateMounts];
}
/***
@@ -91,34 +117,56 @@ class CodeRadio {
* the metadata is updated.
*/
set currentSong(songData = {}) {
- this._currentSong = songData;
- this.renderMetadata();
+ throw new Error('You cannot set the value of a readonly attribute');
}
get currentSong() {
- return this._currentSong;
+ return this[_currentSong];
}
/***
* In order to get the constant durations, we simply take the
* duration for the max of the meter and set the played at to 0
*/
- set played_at(t = 0) {
- this._songStartedAt = t * 1000; // Time comes in a seconds so we multiply by 1000 to set millis
- this.meta.duration.value = 0;
+ set playedAt(t = 0) {
+ throw new Error('You cannot set the value of a readonly attribute');
}
- get played_at() {
- return this._songStartedAt;
+ get playedAt() {
+ return this[_songStartedAt];
}
set duration(d = 0) {
- this._songDuration = d;
- this.meta.duration.max = this._songDuration;
+ throw new Error('You cannot set the value of a readonly attribute');
}
get duration() {
- return this._songDuration;
+ return this[_songDuration];
+ }
+
+ set listeners(v = 0) {
+ throw new Error('You cannot set the value of a readonly attribute');
+ }
+
+ get listeners() {
+ return this[_listeners];
+ }
+
+ get playing() {
+ return !this[_player].paused;
+ }
+
+ get paused() {
+ return this[_player].paused;
+ }
+
+ setMountToConnection() {
+ this[_fastConnection] = (!!navigator.connection) ? (navigator.connection.downlink > 1.5) : false;
+ if (this[_fastConnection]) {
+ this.url = this.mounts.find(mount => !!mount.is_default).url;
+ } else {
+ this.url = this.mounts.find(mount => mount.bitrate < this.mounts.find(m => !!m.is_default).bitrate).url || this.mounts.find(mount => !!mount.is_default).url;
+ }
}
getNowPlaying() {
@@ -129,26 +177,21 @@ class CodeRadio {
np = np[0]; // There is only ever 1 song "Now Playing" so let's simplify the response
// We look through the available mounts to find the default mount (or just the listen_url)
- if (this.url === "")
- this.url = np.station.mounts.find(mount => !!mount.is_default).url;
+ if (this.url === "") {
+ this[_alternateMounts] = np.station.mounts;
+ this.setMountToConnection();
+ }
// We only need to update th metadata if the song has been changed
- if (
- !this.currentSong.id ||
- np.now_playing.song.id !== this.currentSong.id
- ) {
- this.currentSong = np.now_playing.song;
- this.played_at = np.now_playing.played_at;
- this.duration = np.now_playing.duration;
- this.meta.listeners.textContent = `coders listening right now: ${
- np.listeners.current
- }`;
- if (!this._progressInterval) {
- this._progressInterval = setInterval(
- () => this.updateProgress(),
- 100
- );
+ if (np.now_playing.song.id !== this.currentSong.id) {
+ this[_currentSong] = np.now_playing.song;
+ this[_songStartedAt] = np.now_playing.played_at * 1000;
+ this[_songDuration] = np.now_playing.duration;
+ if (this[_listeners] !== np.listeners.current) {
+ this[_listeners] = np.listeners.current;
+ this.emit('listeners', this[_listeners]);
}
+ this.emit('newSong', this[_currentSong]);
}
// Since the server doesn't have a socket connection (yet), we need to long poll it for the current song
@@ -160,23 +203,6 @@ class CodeRadio {
});
}
- /***
- * Yay, let's get some keyboard shortcuts in this tool
- */
- setupEventListeners() {
- document.addEventListener("keydown", evt => this.keyboardControl(evt));
-
- // In order to get around some mobile browser limitations, we can only generate a lot
- // of the audio context stuff AFTER the audio has been triggered. We can't see it until
- // then anyway so it makes no difference to desktop.
- this._player.addEventListener("play", () => {
- if (!this.eq.context) {
- this.initiateEQ();
- this.createVisualizer();
- }
- });
- }
-
keyboardControl(evt = {}) {
// Quick note: if you're wanting to do similar in your projects, keyCode use to be the
// standard however it is being depricated for the key attribute
@@ -186,111 +212,27 @@ class CodeRadio {
this.togglePlay();
break;
case "ArrowUp":
- this.setTargetVolume(
- Math.min(this.audioConfig.maxVolume + this.audioConfig.volumeSteps, 1)
- );
+ this.setTargetVolume(Math.min(this[_audioConfig].maxVolume + this[_audioConfig].volumeSteps, 1));
break;
case "ArrowDown":
- this.setTargetVolume(
- Math.max(this.audioConfig.maxVolume - this.audioConfig.volumeSteps, 0)
- );
+ this.setTargetVolume(Math.max(this[_audioConfig].maxVolume - this[_audioConfig].volumeSteps, 0));
break;
}
}
- initiateEQ() {
- // Create a new Audio Context element to read the samples from
- this.eq.context = new AudioContext();
-
- // Apply the audio element as the source where to pull all the data from
- this.eq.src = this.eq.context.createMediaElementSource(this._player);
-
- // Use some amazing trickery that allows javascript to analyse the current state
- this.eq.analyser = this.eq.context.createAnalyser();
- this.eq.src.connect(this.eq.analyser);
- this.eq.analyser.connect(this.eq.context.destination);
- this.eq.analyser.fftSize = 256;
-
- // Create a buffer array for the number of frequencies available (minus the high pitch useless ones that never really do anything anyway)
- this.eq.bands = new Uint8Array(this.eq.analyser.frequencyBinCount - 32);
- this.updateEQBands();
- }
-
- /***
- * The equalizer bands available need to be updated
- * constantly in order to ensure that the value for any
- * visualizer is up to date.
- */
- updateEQBands() {
- // Populate the buffer with the audio source’s current data
- this.eq.analyser.getByteFrequencyData(this.eq.bands);
-
- // Can’t stop, won’t stop
- requestAnimationFrame(() => this.updateEQBands());
- }
-
- /***
- * When starting the page, the visualizer dom is needed to be
- * created.
- */
- createVisualizer() {
- let container = document.createElement("canvas");
- document.getElementById("visualizer").appendChild(container);
- container.width = container.parentNode.offsetWidth;
- container.height = container.parentNode.offsetHeight;
-
- this.visualizer = {
- ctx: container.getContext("2d"),
- height: container.height,
- width: container.width,
- barWidth: container.width / this.eq.bands.length
- };
-
- this.drawVisualizer();
- }
-
- /***
- * As a base visualizer, the equalizer bands are drawn using
- * canvas in the window directly above the song into.
- */
- drawVisualizer() {
- if (this.eq.bands.reduce((a, b) => a + b, 0) !== 0)
- requestAnimationFrame(() => this.drawVisualizer());
- // Because timeupdate events are not triggered at browser speed, we use requestanimationframe for higher framerates
- else setTimeout(() => this.drawVisualizer(), 250); // If there is no music or audio in the song, then reduce the FPS
-
- let y,
- x = 0; // Intial bar x coordinate
- this.visualizer.ctx.clearRect(
- 0,
- 0,
- this.visualizer.width,
- this.visualizer.height
- ); // Clear the complete canvas
- this.visualizer.ctx.fillStyle = this.config.translucent; // Set the primary colour of the brand (probably moving to a higher object level variable soon)
- this.visualizer.ctx.beginPath(); // Start creating a canvas polygon
- this.visualizer.ctx.moveTo(x, 0); // Start at the bottom left
- this.eq.bands.forEach(band => {
- y = this.config.multiplier * band; // Get the overall hight associated to the current band and convert that into a Y position on the canvas
- this.visualizer.ctx.lineTo(x, y); // Draw a line from the current position to the wherever the Y position is
- this.visualizer.ctx.lineTo(x + this.visualizer.barWidth, y); // Continue that line to meet the width of the bars (canvas width ÷ bar count)
- x += this.visualizer.barWidth; // Add pixels to the x for the next bar
- });
- this.visualizer.ctx.lineTo(x, 0); // Bring the line back down to the bottom of the canvas
- this.visualizer.ctx.fill(); // Fill it
- }
-
play() {
- if (this._player.paused) {
- this._player.volume = 0;
- this._player.play();
+ if (this[_player].paused) {
+ this[_player].volume = 0;
+ this[_player].play();
+ this.emit('play');
this.fadeUp();
return this;
}
}
pause() {
- this._player.pause();
+ this[_player].pause();
+ this.emit('pause');
return this;
}
@@ -301,31 +243,25 @@ class CodeRadio {
*/
togglePlay() {
// If there already is a source, confirm it’s playing or not
- if (!!this._player.src) {
+ if (!!this[_player].src) {
// If the player is paused, set the volume to 0 and fade up
- if (this._player.paused) {
- this._player.volume = 0;
- this._player.play();
- this.fadeUp();
-
+ if (this[_player].paused) this.play();
// if it is already playing, fade the music out (resulting in a pause)
- } else this.fade();
+ else this.fade();
}
return this;
}
setTargetVolume(v) {
- this.audioConfig.maxVolume = parseFloat(
- Math.max(0, Math.min(1, v).toFixed(1))
- );
- this._player.volume = this.audioConfig.maxVolume;
+ this[_audioConfig].maxVolume = parseFloat(Math.max(0, Math.min(1, v).toFixed(1)));
+ this[_player].volume = this[_audioConfig].maxVolume;
+ this.emit('volumeChange', this[_player].volume);
}
// Simple fade command to initiate the playing and pausing in a more fluid method
fade(direction = "down") {
- this.audioConfig.targetVolume =
- direction.toLowerCase() === "up" ? this.audioConfig.maxVolume : 0;
+ this[_audioConfig].targetVolume = direction.toLowerCase() === "up" ? this[_audioConfig].maxVolume : 0;
this.updateVolume();
return this;
}
@@ -341,50 +277,58 @@ class CodeRadio {
// In order to have nice fading, this method adjusts the volume dynamically over time.
updateVolume() {
// In order to fix floating math issues, we set the toFixed in order to avoid 0.999999999999 increments
- let currentVolume = parseFloat(this._player.volume.toFixed(1));
+ let currentVolume = parseFloat(this[_player].volume.toFixed(1));
// If the volume is correctly set to the target, no need to change it
- if (currentVolume === this.audioConfig.targetVolume) {
+ if (currentVolume === this[_audioConfig].targetVolume) {
// If the audio is set to 0 and it’s been met, pause the audio
- if (this.audioConfig.targetVolume === 0) this._player.pause();
+ if (this[_audioConfig].targetVolume === 0) this.pause();
// Unmet audio volume settings require it to be changed
} else {
// We capture the value of the next increment by either the configuration or the difference between the current and target if it's smaller than the increment
- let volumeNextIncrement = Math.min(
- this.audioConfig.volumeSteps,
- Math.abs(this.audioConfig.targetVolume - this._player.volume)
- );
+ let volumeNextIncrement = Math.min(this[_audioConfig].volumeSteps, Math.abs(this[_audioConfig].targetVolume - this[_player].volume));
// Adjust the audio based on if the target is higher or lower than the current
- this._player.volume +=
- this.audioConfig.targetVolume > this._player.volume
+ this[_player].volume +=
+ this[_audioConfig].targetVolume > this[_player].volume
? volumeNextIncrement
: -volumeNextIncrement;
+
+ this.emit('volumeChange', this[_player].volume);
// The speed at which the audio lowers is also controlled.
- setTimeout(
- () => this.updateVolume(),
- this.audioConfig.volumeTransitionSpeed
- );
+ setTimeout(() => this.updateVolume(), this[_audioConfig].volumeTransitionSpeed);
}
}
+
+ on(trigger, fn, once = false) {
+ if (typeof fn != 'function') throw new Error(`Invalid Listener: ${trigger}. Must be a function`);
+ if (!this[_events]) this[_events] = {};
+ if (!this[_events][trigger]) this[_events][trigger] = new Array();
+ this[_events][trigger].push({
+ listener: fn,
+ once: !!once
+ });
+ }
- renderMetadata() {
- if (!!this._currentSong.art) {
- this.meta.picture.style.backgroundImage = `url(${this._currentSong.art})`;
- this.meta.container.classList.add("thumb");
- } else {
- this.meta.container.classList.remove("thumb");
- this.meta.picture.style.backgroundImage = "";
- }
- this.meta.title.textContent = this._currentSong.title;
- this.meta.artist.textContent = this._currentSong.artist;
- this.meta.album.textContent = this._currentSong.album;
+ once(trigger, fn) {
+ this.on(trigger, fn, true);
+ }
+
+ off(trigger, fn) {
+ if (!this[_events] || !this[_events][trigger]) return;
+ this[_events][trigger] = this[_events][trigger].map(evt => (evt !== fn));
}
- updateProgress() {
- this.meta.duration.value =
- (new Date().valueOf() - this._songStartedAt) / 1000;
+ emit(trigger, data) {
+ return new Promise((resolve, reject) => {
+ if (!this[_events] || !this[_events][trigger]) return;
+ this[_events][trigger].forEach((evt, i) => {
+ evt.listener(data);
+ if (evt.once) this[_events][trigger].splice(i, 1);
+ });
+ resolve();
+ });
}
}
diff --git a/visualizer.js b/visualizer.js
new file mode 100644
index 0000000..8f16dd0
--- /dev/null
+++ b/visualizer.js
@@ -0,0 +1,102 @@
+const _player = Symbol('player')
+
+export class Visualizer {
+
+ constructor(fCC_Player, container = false) {
+ this[_player] = fCC_Player.player;
+ this.eq = {};
+ this.config = {
+ baseColour: "rgb(10, 10, 35)",
+ translucent: "rgba(10, 10, 35, 0.6)",
+ multiplier: 0.7529,
+ };
+ this.container = container || document.getElementById("visualizer");
+
+ // In order to get around some mobile browser limitations, we can only generate a lot
+ // of the audio context stuff AFTER the audio has been triggered. We can't see it until
+ // then anyway so it makes no difference to desktop.
+ fCC_Player.player.addEventListener('play', () => {
+ if (!this.eq.context) {
+ this.initiateEQ();
+ this.createVisualizer();
+ }
+ })
+
+ }
+
+ initiateEQ() {
+ // Create a new Audio Context element to read the samples from
+ this.eq.context = new AudioContext();
+ // Apply the audio element as the source where to pull all the data from
+ this.eq.src = this.eq.context.createMediaElementSource(this[_player]);
+
+ // Use some amazing trickery that allows javascript to analyse the current state
+ this.eq.analyser = this.eq.context.createAnalyser();
+ this.eq.src.connect(this.eq.analyser);
+ this.eq.analyser.connect(this.eq.context.destination);
+ this.eq.analyser.fftSize = 256;
+
+ // Create a buffer array for the number of frequencies available (minus the high pitch useless ones that never really do anything anyway)
+ this.eq.bands = new Uint8Array(this.eq.analyser.frequencyBinCount - 32);
+ this.updateEQBands();
+ }
+
+ /***
+ * The equalizer bands available need to be updated
+ * constantly in order to ensure that the value for any
+ * visualizer is up to date.
+ */
+ updateEQBands() {
+ // Populate the buffer with the audio source’s current data
+ this.eq.analyser.getByteFrequencyData(this.eq.bands);
+
+ // Can’t stop, won’t stop
+ requestAnimationFrame(() => this.updateEQBands());
+ }
+
+ /***
+ * When starting the page, the visualizer dom is needed to be
+ * created.
+ */
+ createVisualizer() {
+ let container = document.createElement("canvas");
+ this.container.appendChild(container);
+ container.width = container.parentNode.offsetWidth;
+ container.height = container.parentNode.offsetHeight;
+
+ this.visualizer = {
+ ctx: container.getContext("2d"),
+ height: container.height,
+ width: container.width,
+ barWidth: container.width / this.eq.bands.length
+ };
+
+ this.drawVisualizer();
+ }
+
+ /***
+ * As a base visualizer, the equalizer bands are drawn using
+ * canvas in the window directly above the song into.
+ */
+ drawVisualizer() {
+ if (this.eq.bands.reduce((a, b) => a + b, 0) !== 0) requestAnimationFrame(() => this.drawVisualizer());
+ // Because timeupdate events are not triggered at browser speed, we use requestanimationframe for higher framerates
+ else setTimeout(() => this.drawVisualizer(), 250); // If there is no music or audio in the song, then reduce the FPS
+
+ let y,
+ x = 0; // Intial bar x coordinate
+ this.visualizer.ctx.clearRect(0, 0, this.visualizer.width, this.visualizer.height); // Clear the complete canvas
+ this.visualizer.ctx.fillStyle = this.config.translucent; // Set the primary colour of the brand (probably moving to a higher object level variable soon)
+ this.visualizer.ctx.beginPath(); // Start creating a canvas polygon
+ this.visualizer.ctx.moveTo(x, 0); // Start at the bottom left
+ this.eq.bands.forEach(band => {
+ y = this.config.multiplier * band; // Get the overall hight associated to the current band and convert that into a Y position on the canvas
+ this.visualizer.ctx.lineTo(x, y); // Draw a line from the current position to the wherever the Y position is
+ this.visualizer.ctx.lineTo(x + this.visualizer.barWidth, y); // Continue that line to meet the width of the bars (canvas width ÷ bar count)
+ x += this.visualizer.barWidth; // Add pixels to the x for the next bar
+ });
+ this.visualizer.ctx.lineTo(x, 0); // Bring the line back down to the bottom of the canvas
+ this.visualizer.ctx.fill(); // Fill it
+ }
+
+}
\ No newline at end of file