Add music player
This commit is contained in:
parent
325175525b
commit
ad92c1866c
|
@ -34,9 +34,12 @@
|
||||||
"webpack-merge": "^4.1.2"
|
"webpack-merge": "^4.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fortawesome/fontawesome": "^1.1.8",
|
||||||
|
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
||||||
"bootstrap": "^4.0.0",
|
"bootstrap": "^4.0.0",
|
||||||
"jquery": "^3.3.1",
|
"jquery": "^3.3.1",
|
||||||
"popper.js": "^1.14.1"
|
"popper.js": "^1.14.1",
|
||||||
|
"three": "^0.93.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "webpack --config webpack.config.js",
|
"dev": "webpack --config webpack.config.js",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
@import 'custom-bootstrap';
|
@import 'custom-bootstrap';
|
||||||
@import 'fonts';
|
@import 'fonts';
|
||||||
@import 'headings';
|
@import 'headings';
|
||||||
|
|
||||||
|
$fa-font-path: "~@fortawesome/fontawesome-free-webfonts/webfonts";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import "bootstrap";
|
import "bootstrap";
|
||||||
import "./music.scss";
|
import "./music.scss";
|
||||||
import music from "./Mseq_-_Journey.mp3";
|
|
||||||
|
|
||||||
|
import music from "./Mseq_-_Journey.mp3";
|
||||||
import Player from "./player/index.js";
|
import Player from "./player/index.js";
|
||||||
|
|
||||||
window.player = new Player(music);
|
window.player = new Player(music);
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
extends ../lib/pug/base
|
extends ../lib/pug/base
|
||||||
|
|
||||||
block content
|
block footer
|
||||||
#playerUI
|
#playerUI.container-fluid
|
||||||
|
.row
|
||||||
|
#playerContent.container-fluid
|
||||||
|
#playerControls.container-fluid.fixed-bottom
|
||||||
|
.row
|
||||||
|
span#playerButton.col-1.playerControlsContent.fa.fa-fw.fa-spin.fa-spinner
|
||||||
|
#playerText.col-11.playerControlsContent Some text for now
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
@import "../lib/scss/main";
|
@import "../lib/scss/main";
|
||||||
|
@import "~@fortawesome/fontawesome-free-webfonts/scss/fontawesome.scss";
|
||||||
|
@import "~@fortawesome/fontawesome-free-webfonts/scss/fa-solid.scss";
|
||||||
@import "~bootstrap/scss/bootstrap";
|
@import "~bootstrap/scss/bootstrap";
|
||||||
|
|
||||||
#playerUI {
|
#playerUI {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#playerContent {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playerControls {
|
||||||
|
background-color: $dark;
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerControlsContent {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#playerButton {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
55
src/music/player/audio_manager.js
Normal file
55
src/music/player/audio_manager.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/**
|
||||||
|
* AudioManager()
|
||||||
|
*
|
||||||
|
* A class to manage the audio stream.
|
||||||
|
*
|
||||||
|
* @param {string} src - The URL of the audio to play.
|
||||||
|
*/
|
||||||
|
class AudioManager {
|
||||||
|
constructor(src) {
|
||||||
|
this._muted = false;
|
||||||
|
|
||||||
|
// Create audio graph
|
||||||
|
this._context = new AudioContext();
|
||||||
|
this.audio = new Audio(src);
|
||||||
|
this._volume = this._context.createGain();
|
||||||
|
this._source = this._context.createMediaElementSource(this.audio);
|
||||||
|
|
||||||
|
let context = this._context;
|
||||||
|
let volume = this._volume;
|
||||||
|
let source = this._source;
|
||||||
|
|
||||||
|
source.connect(volume);
|
||||||
|
volume.connect(context.destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
get context() {
|
||||||
|
return this._context;
|
||||||
|
}
|
||||||
|
|
||||||
|
get source() {
|
||||||
|
return this._source;
|
||||||
|
}
|
||||||
|
|
||||||
|
get muted() {
|
||||||
|
return this._muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(...args) {
|
||||||
|
this.audio.addEventListener(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
mute() {
|
||||||
|
let context = this._context;
|
||||||
|
let volume = this._volume;
|
||||||
|
|
||||||
|
if (this._muted)
|
||||||
|
volume.gain.setValueAtTime(1, context.currentTime);
|
||||||
|
else
|
||||||
|
volume.gain.setValueAtTime(0, context.currentTime);
|
||||||
|
|
||||||
|
this._muted = !this._muted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioManager;
|
16
src/music/player/background.js
Normal file
16
src/music/player/background.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
class Background {
|
||||||
|
constructor(display, audioManager) {
|
||||||
|
if (this.constructor === Background)
|
||||||
|
throw new Error("Cannot instantiate abstract class!");
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Background;
|
99
src/music/player/backgrounds/Spectrum.js
Normal file
99
src/music/player/backgrounds/Spectrum.js
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import * as three from "three";
|
||||||
|
|
||||||
|
import Background from "../background";
|
||||||
|
|
||||||
|
class Spectrum extends Background {
|
||||||
|
constructor(display, audioManager) {
|
||||||
|
super(audioManager, display);
|
||||||
|
|
||||||
|
this._audioManager = audioManager;
|
||||||
|
this._display = display;
|
||||||
|
|
||||||
|
this._init_analyser();
|
||||||
|
this._init_scene();
|
||||||
|
this._init_objects();
|
||||||
|
}
|
||||||
|
|
||||||
|
_init_analyser() {
|
||||||
|
let audioManager = this._audioManager;
|
||||||
|
|
||||||
|
let analyser = audioManager.context.createAnalyser();
|
||||||
|
analyser.fftSize = 2048;
|
||||||
|
analyser.smoothingTimeConstant = .8;
|
||||||
|
|
||||||
|
let analyser_data = new Float32Array(analyser.frequencyBinCount);
|
||||||
|
|
||||||
|
audioManager.source.connect(analyser);
|
||||||
|
analyser.getFloatFrequencyData(analyser_data);
|
||||||
|
|
||||||
|
this._analyser = analyser;
|
||||||
|
this._analyser_data = analyser_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_init_scene() {
|
||||||
|
let scene = new three.Scene();
|
||||||
|
|
||||||
|
let camera = new three.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 10);
|
||||||
|
camera.position.z = 1;
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
let renderer = new three.WebGLRenderer({antialias: true});
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
|
this._display.append(renderer.domElement);
|
||||||
|
|
||||||
|
this._scene = scene;
|
||||||
|
this._camera = camera;
|
||||||
|
this._renderer = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
_init_objects() {
|
||||||
|
let analyser = this._analyser;
|
||||||
|
let scene = this._scene;
|
||||||
|
|
||||||
|
let boxes = Array(analyser.frequencyBinCount);
|
||||||
|
let width = 2 / analyser.frequencyBinCount;
|
||||||
|
|
||||||
|
for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
|
||||||
|
let geometry = new three.BoxGeometry(1, 1, 1);
|
||||||
|
let material = new three.MeshBasicMaterial({color: 0x99d1ce});
|
||||||
|
let cube = new three.Mesh(geometry, material);
|
||||||
|
|
||||||
|
cube.scale.set(width, 1e-6, width);
|
||||||
|
cube.position.set(-1 + freq * width, 0, 0);
|
||||||
|
|
||||||
|
scene.add(cube);
|
||||||
|
boxes[freq] = cube;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._boxes = boxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
requestAnimationFrame(this._render.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
_render() {
|
||||||
|
let analyser = this._analyser;
|
||||||
|
let camera = this._camera;
|
||||||
|
let renderer = this._renderer;
|
||||||
|
let scene = this._scene;
|
||||||
|
|
||||||
|
for (let freq = 0; freq < analyser.frequencyBinCount; freq++) {
|
||||||
|
let height = analyser.maxDecibels / this._analyser_data[freq];
|
||||||
|
|
||||||
|
if (height > .3)
|
||||||
|
height -= .3;
|
||||||
|
else
|
||||||
|
height = 1e-6;
|
||||||
|
|
||||||
|
this._boxes[freq].scale.y = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.render(scene, camera);
|
||||||
|
analyser.getFloatFrequencyData(this._analyser_data);
|
||||||
|
requestAnimationFrame(this._render.bind(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Spectrum;
|
191
src/music/player/controls.js
vendored
Normal file
191
src/music/player/controls.js
vendored
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/**
|
||||||
|
* _Text()
|
||||||
|
*
|
||||||
|
* A helper class to control the text area for audio controls.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {HTMLElement} div - The div of the text area.
|
||||||
|
*/
|
||||||
|
class _Text {
|
||||||
|
constructor (div) {
|
||||||
|
this._element = div;
|
||||||
|
|
||||||
|
this._text = "";
|
||||||
|
this._flashing = false;
|
||||||
|
|
||||||
|
this._flash_timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashText(text) {
|
||||||
|
let element = this._element;
|
||||||
|
let flash_timeout = this._flash_timeout;
|
||||||
|
|
||||||
|
element.html(text);
|
||||||
|
this._flashing = true;
|
||||||
|
|
||||||
|
clearTimeout(flash_timeout);
|
||||||
|
this._flash_timeout = setTimeout(() => {
|
||||||
|
this._flashing = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(text) {
|
||||||
|
let element = this._element;
|
||||||
|
let flashing = this._flashing;
|
||||||
|
|
||||||
|
if (!flashing)
|
||||||
|
element.html(text);
|
||||||
|
|
||||||
|
this._text = text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* _Button()
|
||||||
|
*
|
||||||
|
* A helper class to control the indicator button for audio controls.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {HTMLElement} button - The div of the button.
|
||||||
|
*/
|
||||||
|
class _Button {
|
||||||
|
constructor(button) {
|
||||||
|
this._element = button;
|
||||||
|
|
||||||
|
this._classes = "";
|
||||||
|
this._flash_classes = "";
|
||||||
|
this._flashing = false;
|
||||||
|
|
||||||
|
this._flash_timeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
click (callback) {
|
||||||
|
this._element.click(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
flashIcon(icon) {
|
||||||
|
let classes = this._classes;
|
||||||
|
let element = this._element;
|
||||||
|
let flash_classes = this._flash_classes;
|
||||||
|
let flash_timeout = this._flash_timeout;
|
||||||
|
|
||||||
|
// We remove any potentially current classes, and add the
|
||||||
|
// requested icon class.
|
||||||
|
element.removeClass(flash_classes);
|
||||||
|
element.removeClass(classes);
|
||||||
|
element.addClass(icon);
|
||||||
|
|
||||||
|
// We keep track of the classes we are flashing and the fact
|
||||||
|
// that we are flashing, so that we can remove the correct
|
||||||
|
// classes later.
|
||||||
|
this._flash_classes = icon;
|
||||||
|
this._flashing = true;
|
||||||
|
|
||||||
|
// If we previously started a timeout, we'll want to cancel it
|
||||||
|
// now, since otherwise *it* will clear out our classes.
|
||||||
|
clearTimeout(flash_timeout);
|
||||||
|
|
||||||
|
// We start a timeout to reset to the original classes.
|
||||||
|
this._flash_timeout = setTimeout(() => {
|
||||||
|
element.removeClass(this._flash_classes);
|
||||||
|
element.addClass(this._classes);
|
||||||
|
|
||||||
|
this._flash_classes = "";
|
||||||
|
this._flashing = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIcon(icon) {
|
||||||
|
let classes = this._classes;
|
||||||
|
let element = this._element;
|
||||||
|
let flashing = this._flashing;
|
||||||
|
|
||||||
|
// If we are currently flashing an icon, we'll hold off on
|
||||||
|
// changing the icon.
|
||||||
|
if (!flashing) {
|
||||||
|
element.removeClass(classes);
|
||||||
|
element.addClass(icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._classes = icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controls()
|
||||||
|
*
|
||||||
|
* A class to manage music player controls.
|
||||||
|
*
|
||||||
|
* @param {AudioManager} audioManager - The audio manager to interact with.
|
||||||
|
*/
|
||||||
|
class Controls {
|
||||||
|
constructor(audioManager) {
|
||||||
|
this._element = $("#playerControls");
|
||||||
|
this._button = new _Button($("#playerControls #playerButton"));
|
||||||
|
this._text = new _Text($("#playerControls #playerText"));
|
||||||
|
this._audioManager = audioManager;
|
||||||
|
|
||||||
|
this._setAudioEvents({
|
||||||
|
canplay: () => this._audioManager.audio.play(),
|
||||||
|
error: () => this.setError(this._audioManager.audio.error.message),
|
||||||
|
loadstart: () => this.setLoading(),
|
||||||
|
playing: () => this.setPlaying(),
|
||||||
|
stalled: () => this.setLoading("Stalling")
|
||||||
|
});
|
||||||
|
|
||||||
|
this._button.click(() => this.mute());
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlaying() {
|
||||||
|
let button = this._button;
|
||||||
|
let text = this._text;
|
||||||
|
|
||||||
|
button.setIcon("fa-play");
|
||||||
|
text.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(reason="Loading") {
|
||||||
|
let button = this._button;
|
||||||
|
let text = this._text;
|
||||||
|
|
||||||
|
button.setIcon("fa-spin fa-spinner");
|
||||||
|
text.setText(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(reason="") {
|
||||||
|
let button = this._button;
|
||||||
|
let text = this._text;
|
||||||
|
|
||||||
|
if (reason == "")
|
||||||
|
reason = "Error";
|
||||||
|
else
|
||||||
|
reason = `Error: ${reason}`;
|
||||||
|
|
||||||
|
button.setIcon("fa-stop-circle");
|
||||||
|
text.setText(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
mute() {
|
||||||
|
let audioManager = this._audioManager;
|
||||||
|
let button = this._button;
|
||||||
|
let text = this._text;
|
||||||
|
|
||||||
|
audioManager.mute();
|
||||||
|
|
||||||
|
if (audioManager.muted) {
|
||||||
|
button.flashIcon("fa-volume-off");
|
||||||
|
text.flashText("Muted");
|
||||||
|
} else {
|
||||||
|
button.flashIcon("fa-volume-up");
|
||||||
|
text.flashText("Unmuted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setAudioEvents(events) {
|
||||||
|
for (let [event, handler] of Object.entries(events)) {
|
||||||
|
this._audioManager.addEventListener(event, handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Controls;
|
21
src/music/player/display.js
Normal file
21
src/music/player/display.js
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import Spectrum from "./backgrounds/Spectrum.js";
|
||||||
|
|
||||||
|
let backgrounds = {
|
||||||
|
"Spectrum": Spectrum
|
||||||
|
};
|
||||||
|
|
||||||
|
class Display {
|
||||||
|
constructor(audioManager) {
|
||||||
|
this._audioManager = audioManager;
|
||||||
|
this._element = $("#playerContent");
|
||||||
|
|
||||||
|
this._background = new backgrounds["Spectrum"](this._element, audioManager);
|
||||||
|
this._background.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
set background(name) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Display;
|
|
@ -1,28 +1,13 @@
|
||||||
class AudioManager {
|
import AudioManager from "./audio_manager";
|
||||||
constructor(src) {
|
import Controls from "./controls";
|
||||||
this._context = new AudioContext();
|
import Display from "./display";
|
||||||
this.audio = new Audio(src);
|
|
||||||
|
|
||||||
let audio = this.audio;
|
|
||||||
let context = this._context;
|
|
||||||
|
|
||||||
// Create audio graph
|
|
||||||
let volume = context.createGain();
|
|
||||||
let source = context.createMediaElementSource(audio);
|
|
||||||
|
|
||||||
source.connect(volume);
|
|
||||||
volume.connect(context.destination);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Player {
|
class Player {
|
||||||
constructor(src="https://tlater.net/assets/Mseq_-_Journey.mp3") {
|
constructor(src="https://tlater.net/assets/Mseq_-_Journey.mp3") {
|
||||||
this._ui = $("#playerUI");
|
this._ui = $("#playerUI");
|
||||||
this._audioManager = new AudioManager(src);
|
this._audioManager = new AudioManager(src);
|
||||||
|
this._controls = new Controls(this._audioManager);
|
||||||
let audioManager = this._audioManager;
|
this._display = new Display(this._audioManager);
|
||||||
|
|
||||||
audioManager.oncanplay = () => audioManager.audio.play();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue