Add music player

This commit is contained in:
Tristan Maat 2018-06-11 01:37:36 +00:00
parent 325175525b
commit ad92c1866c
12 changed files with 989 additions and 38 deletions

View file

@ -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",

View file

@ -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";

View file

@ -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);

View file

@ -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

View file

@ -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;
}

View 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;

View 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;

View 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
View 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;

View 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;

View file

@ -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();
} }
} }

578
yarn.lock

File diff suppressed because it is too large Load diff