Add music player
This commit is contained in:
parent
325175525b
commit
ad92c1866c
|
@ -34,9 +34,12 @@
|
|||
"webpack-merge": "^4.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome": "^1.1.8",
|
||||
"@fortawesome/fontawesome-free-webfonts": "^1.0.9",
|
||||
"bootstrap": "^4.0.0",
|
||||
"jquery": "^3.3.1",
|
||||
"popper.js": "^1.14.1"
|
||||
"popper.js": "^1.14.1",
|
||||
"three": "^0.93.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "webpack --config webpack.config.js",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@import 'custom-bootstrap';
|
||||
@import 'fonts';
|
||||
@import 'headings';
|
||||
|
||||
$fa-font-path: "~@fortawesome/fontawesome-free-webfonts/webfonts";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import "bootstrap";
|
||||
import "./music.scss";
|
||||
import music from "./Mseq_-_Journey.mp3";
|
||||
|
||||
import music from "./Mseq_-_Journey.mp3";
|
||||
import Player from "./player/index.js";
|
||||
|
||||
window.player = new Player(music);
|
||||
|
|
|
@ -1,4 +1,10 @@
|
|||
extends ../lib/pug/base
|
||||
|
||||
block content
|
||||
#playerUI
|
||||
block footer
|
||||
#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 "~@fortawesome/fontawesome-free-webfonts/scss/fontawesome.scss";
|
||||
@import "~@fortawesome/fontawesome-free-webfonts/scss/fa-solid.scss";
|
||||
@import "~bootstrap/scss/bootstrap";
|
||||
|
||||
#playerUI {
|
||||
width: 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 {
|
||||
constructor(src) {
|
||||
this._context = new AudioContext();
|
||||
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);
|
||||
}
|
||||
}
|
||||
import AudioManager from "./audio_manager";
|
||||
import Controls from "./controls";
|
||||
import Display from "./display";
|
||||
|
||||
class Player {
|
||||
constructor(src="https://tlater.net/assets/Mseq_-_Journey.mp3") {
|
||||
this._ui = $("#playerUI");
|
||||
this._audioManager = new AudioManager(src);
|
||||
|
||||
let audioManager = this._audioManager;
|
||||
|
||||
audioManager.oncanplay = () => audioManager.audio.play();
|
||||
this._controls = new Controls(this._audioManager);
|
||||
this._display = new Display(this._audioManager);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Reference in a new issue