This commit is contained in:
Maurice Grönwoldt 2020-08-01 21:51:54 +02:00
commit 300b6c4106
30 changed files with 1399 additions and 29 deletions

1
raw/gui/wave.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -1,20 +1,28 @@
const shaderHandler = new ShaderHandler(null),
audioHandler = new AudioHandler(),
gui = new GUI(),
player = new Player();
visual = new VisualDrawer(),
template = new Template(),
player = new Player(),
config = new Config();
let c = null,
gl = null;
async function startUP() {
let c = document.body.querySelector('#c'),
c = document.body.querySelector('#c'),
gl = c.getContext("webgl2");
if (!gl) {
alert("SORRY THE BROWSER DOESN'T SUPPORT WEBGL2");
return false;
}
shaderHandler.setGL(gl)
await shaderHandler.loadArray(["wave", "sphere"], 'shaders/');
await shaderHandler.loadArray(["wave", "sphere", "water"], 'shaders/');
await audioHandler.init();
await player.init();
gui.init();
await visual.init();
await gui.init();
await initHandler();
}
startUP().then(r => {

View file

@ -1,5 +1,71 @@
const AudioContext = window.AudioContext || window.webkitAudioContext;
class AudioHandler {
async init() {
this.audioFile = new Audio();
let self = this;
self.isStarted = false;
self.audioFile = new Audio();
self.actx = new AudioContext()
self.analyser = self.actx.createAnalyser()
self.analyser.fftSize = 4096;
self.lastSong = null;
await self.connectAll();
}
async connectAll() {
let self = this;
self.source = self.actx.createMediaElementSource(self.audioFile);
self.source.connect(self.analyser);
self.analyser.connect(self.actx.destination);
self.audioFile.addEventListener('ended', player.nextSong.bind(player));
}
async start() {
if (this.audioFile.src === '') {
return;
}
if (!this.isStarted) {
this.isStarted = true;
await this.actx.resume();
}
}
async stop() {
if (this.isStarted) {
this.isStarted = false;
await this.actx.suspend();
}
}
fftSize(size) {
this.analyser.fftSize = size;
}
smoothing(float) {
this.analyser.smoothingTimeConstant = float;
}
loadSong(src) {
let self = this;
if (self.lastSong) {
URL.revokeObjectURL(self.lastSong);
}
self.lastSong = this.audioFile.src = URL.createObjectURL(src);
if (!this.isStarted) {
this.start();
}
this.audioFile.play();
}
getIntArray(steps) {
let dataArray = new Uint8Array(steps);
this.analyser.getByteFrequencyData(dataArray);
return dataArray;
}
getFloatArray() {
let dataArray = new Float32Array(this.analyser.frequencyBinCount);
this.analyser.getFloatTimeDomainData(dataArray);
return dataArray;
}
}

35
raw/javascript/config.js Normal file
View file

@ -0,0 +1,35 @@
class Config {
constructor() {
this.config = {};
this.name = ''
}
loadConfigByName(name) {
this.saveConfig();
this.name = 'config-' + name;
this.config = JSON.parse(this.name);
}
saveConfig() {
if (this.name !== '') {
localStorage.setItem(this.name, JSON.stringify(this.config));
}
}
addItem(name, value) {
this.config[name] = value;
}
removeItem(name) {
delete this.config[name];
}
getItem(name, def) {
let value = this.config[name];
if (value === undefined || value === null) {
this.config[name] = def;
value = def;
}
return value;
}
}

View file

@ -0,0 +1,39 @@
async function initHandler() {
let body = $('body');
$('.playlist.menu-icon').addEventListener('click', e => {
player.playlist.renderPagination(player.playlist.page);
});
body.addDelegatedEventListener('click', '.playlist-item', (e, el) => {
let number = el.dataset.index;
player.playByID(parseInt(number));
togglePlayButton('pause');
});
body.addDelegatedEventListener('click', '.controls button', (e, el) => {
switch (el.id) {
case 'previous':
player.prevSong();
break;
case 'next':
player.nextSong()
break;
case 'play':
player.playStop();
break;
}
togglePlayButton(audioHandler.audioFile.paused ? 'play' : 'pause');
});
}
function togglePlayButton(status) {
let icons = $$('#play .icon');
icons.forEach(el => {
if(el.dataset.name === status) {
el.removeClass('hide');
} else {
el.addClass('hide');
}
})
}

View file

@ -1,5 +1,98 @@
class GUI {
init() {
async init() {
this.data = {};
this.modal = new Modal();
// load first vis window!
await this.loadForVis();
await template.loadArray([
'playlist-item',
'playlist',
'playlist-footer'
]);
this.initDropZone();
}
async loadForVis() {
let c = visual.c,
d = this.data[c];
if (d == null) {
this.data[c] = await fetch("out/gui/" + c + ".json").then((r) => r.json());
}
}
renderModal(content, title) {
let modal = $('#modal'),
p = modal.parentNode,
h = $('header .headline', modal),
con = $('modal-content', modal);
h.innerHTML = title;
con.innerHTML = content;
p.classList.remove('hide');
}
initDropZone() {
let items = 'drag dragstart dragend dragover dragenter dragleave drop'.split(' ');
items.forEach(el => {
window.addEventListener(el, async e => {
e.preventDefault();
e.stopPropagation();
if (e.type === 'drop') {
if (e.dataTransfer.files.length > 0) {
player.playlist.changeFiles(e, e.dataTransfer);
} else {
alert("Sorry you need to upload files!");
}
}
});
});
};
}
class Modal {
constructor() {
let self = this;
self.currentModal = '';
self.modal = $('#modal');
self.parent = self.modal.parentNode;
self.modal.addDelegatedEventListener('click', 'header .close', this.closeModal.bind(this));
}
resetModal() {
this.renderModal('', '', '');
}
renderModal(title, content, footer) {
this.currentModal = title;
this.renderHeader(title);
this.renderContent(content);
this.renderFooter(footer);
this.showModal();
}
renderHeader(header) {
let h = $('header .headline', this.modal);
h.innerHTML = header;
}
renderContent(content) {
let con = $('modal-content', this.modal);
con.innerHTML = content;
}
renderFooter(footer) {
let con = $('modal-footer', this.modal);
con.innerHTML = footer;
}
closeModal() {
this.parent.addClass("hide")
}
isCurrent(title) {
return title === this.currentModal;
}
showModal() {
this.parent.removeClass("hide")
}
}

View file

@ -85,6 +85,12 @@ class ShaderHandler {
return this.programs[name];
}
use(name) {
let pro = this.programs[name];
this.gl.useProgram(pro);
return pro;
}
async loadArray(list, path) {
let self = this;
for (const e of list) {

View file

@ -1,5 +1,182 @@
class Player {
async init() {
this.playlist = new Playlist();
}
nextSong() {
let next = this.playlist.getNext();
audioHandler.loadSong(next.file);
}
prevSong() {
let next = this.playlist.getPrevious();
audioHandler.loadSong(next.file);
}
playStop() {
if (!audioHandler.lastSong) {
let next = this.playlist.getCurrent();
audioHandler.loadSong(next.file);
}
let audioFile = audioHandler.audioFile;
if (audioFile.paused) {
audioFile.play();
} else {
audioFile.pause();
}
}
playByID(number) {
let next = this.playlist.getFile(number);
audioHandler.loadSong(next.file);
}
}
const PAGINATIONLIMIT = 50;
class Playlist {
constructor() {
this.list = [];
this.shuffled = [];
this.index = 0;
this.page = 0;
this.isShuffle = false;
$('body').addDelegatedEventListener('change', 'input[type="file"]', this.changeFiles.bind(this));
$('body').addDelegatedEventListener('click', '.pagination .item', this.handlePagination.bind(this));
}
shuffle() {
// only shuffle if more then 2 elements are in
let len = this.list.length;
if (len < 3) {
this.shuffled = this.list;
}
// the current-list need to be shuffled...
for (let i = 0; i < len; i++) {
let random = VTUtils.randomInt(0, len - 1);
this.swap(i, random);
}
}
swap(a, b) {
this.shuffled[a] = this.list[b];
this.shuffled[b] = this.list[a];
}
getNext() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index + 1;
if (next > len) {
next = 0;
}
this.index = next;
return items[next];
}
getPrevious() {
let items = this.isShuffle ? this.shuffled : this.list,
len = items.length - 1,
next = this.index - 1;
if (next < 0) {
next = len;
}
this.index = next;
return items[next];
}
getCurrent() {
let items = this.isShuffle ? this.shuffled : this.list;
return items[this.index];
}
getFile(index) {
let items = this.isShuffle ? this.shuffled : this.list;
return items[index];
}
// on new upload... this has to be an array!
setPlaylist(files) {
this.index = 0;
this.list = files;
this.shuffle();
}
handlePagination(event, el) {
if (el.hasClass('inactive')) {
return;
}
if (el.hasClass('next-site')) {
this.renderPagination(this.page + 1);
} else {
this.renderPagination(this.page - 1);
}
}
renderPagination(page) {
let length = this.list.length,
maxSite = Math.ceil(length / PAGINATIONLIMIT) - 1;
if (page < 0) {
page = 0;
}
if (page > maxSite) {
page = maxSite;
}
let s = page * PAGINATIONLIMIT,
e = s + PAGINATIONLIMIT,
data = "";
this.page = page;
if (e >= length) {
e = length;
}
if (length > 0) {
let items = this.isShuffle ? this.shuffled : this.list;
for (let i = s; i < e; i++) {
let obj = {
index: i,
nr: i + 1,
title: items[i].name
}
data += template.parseTemplate("playlist-item", obj);
}
} else {
data = "<h1>No Songs uploaded!</h1>";
}
let hasNext = maxSite > 1 && page < maxSite;
gui.modal.renderModal(
"Playlist",
template.parseTemplate("playlist", {
content: data,
}),
template.parseTemplate('playlist-footer', {
prevActive: page > 0 ? 'active' : 'inactive',
nextActive: hasNext ? 'active' : 'inactive',
page: (page + 1) + ' / ' + parseInt(maxSite + 1),
})
);
}
//playlist handler for file input!
changeFiles(e, el) {
let files = [];
let i = 0;
for (let file of el.files) {
if (file && file.type.indexOf('audio') !== -1 && file.name.match(".m3u") === null) {
let name = file.name.split(".");
name.pop();
name = name.join(".");
files.push({
file: file,
name: name,
index: i++
});
}
}
this.setPlaylist(files);
if (files.length > 0) {
this.renderPagination(0);
} else {
alert("No Valid AudioFiles found!");
}
}
}

View file

@ -0,0 +1,44 @@
class Template {
constructor() {
this.tpl = {};
}
async loadTemplate(name) {
let self = this;
if (!this.tpl[name]) {
await fetch(templateDir + name + ".tpl").then((r) => r.text()).then(c => {
self.tpl[name] = c;
})
}
}
async loadArray(names) {
for (let name of names) {
await this.loadTemplate(name);
}
}
parseTemplate(name, data) {
if (!this.tpl[name]) {
return ""
}
let m, d = this.tpl[name];
while ((m = templateEx.exec(d)) !== null) {
if (m.index === templateEx.lastIndex) {
templateEx.lastIndex++;
}
let key = m[0];
d = d.replace(key, data[m[1]] || "")
}
return d;
}
parseFromAPI(url, name, cb) {
fetch(url).then((r) => r.json()).then(d => {
cb(this.parseTemplate(name, d))
}).catch(console.error)
}
}
const templateEx = /\$(.*?)\$/gm;
const templateDir = "out/tpl/"

View file

@ -260,6 +260,16 @@ Node.prototype.addDelegatedEventListener = function (type, aim, cb) {
})
};
Node.prototype.hasClass = function (className) {
return this.classList.contains(className);
}
Node.prototype.addClass = function (className) {
return this.classList.add(className);
}
Node.prototype.removeClass = function (className) {
return this.classList.remove(className);
}
function create(name, content) {
let d = document.createElement(name);
if (content) {

View file

@ -3,6 +3,11 @@ class Visual {
this.data = []; //for drawing
this.dataArray = [];
}
updateData() {
}
draw() {
}
@ -11,5 +16,33 @@ class Visual {
}
class VisualDrawer {
constructor() {
this.visuals = {
"sphere": new Sphere(),
"wave": new Wave(),
"water": new Water()
}
this.c = "wave";
}
init() {
this.visuals[this.c].setup();
this.updateLoop();
}
switch(visual) {
if (this.visuals[visual] != null) {
this.c = visual;
this.visuals[this.c].setup();
}
}
updateLoop() {
let self = this;
let pro = shaderHandler.use(self.c);
let vis = self.visuals[self.c];
vis.updateData();
vis.draw(pro);
requestAnimationFrame(self.updateLoop.bind(self))
}
}

View file

@ -0,0 +1,10 @@
//animate Water the way like the Audio is Coming... 256FFT-Size max!
class Water extends Visual {
draw() {
}
setup() {
audioHandler.fftSize(256)
}
}

View file

@ -0,0 +1,60 @@
// 3D Audio-Waves -> maybe also 2D?
class Wave extends Visual {
updateData() {
this.data = [];
let data = audioHandler.getFloatArray();
let add = 2 / data.length,
x = -1;
for (let i = 0; i < data.length; i++) {
this.data.push(x, data[i], data[i]);
x += add;
}
}
draw(program) {
c.width = window.innerWidth;
c.height = window.innerHeight;
this.prepare(program);
let position = this.position,
color = this.color,
positionBuffer = gl.createBuffer();
this.rotate(program);
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.data), gl.DYNAMIC_DRAW);
let vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(position);
gl.vertexAttribPointer(position, 3, gl.FLOAT, true, 0, 0);
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
gl.clearDepth(2.0)
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.drawArrays(gl.LINE_STRIP || gl.POINTS, 0, this.data.length / 3);
}
rotate(program) {
let aspect = c.height / c.width,
matrix = [
1 / aspect, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
]
matrix = TDUtils.multiply(matrix, TDUtils.xRotation(config.getItem("xRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.yRotation(config.getItem("yRotate", 0)));
matrix = TDUtils.multiply(matrix, TDUtils.zRotation(config.getItem("zRotate", 0)));
let rotate = gl.getUniformLocation(program, "u_matrix");
gl.uniformMatrix4fv(rotate, false, matrix);
}
setup() {
audioHandler.fftSize(16384)
}
prepare(program) {
this.position = gl.getAttribLocation(program, "a_position");
this.color = gl.getUniformLocation(program, "u_color");
}
}

View file

@ -50,6 +50,7 @@ group-input {
.top-menu-left {
display: flex;
div {
position: relative;
}
@ -105,4 +106,63 @@ group-input {
51%, 100% {
transform-origin: right;
}
}
.grey-screen {
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, .5);
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
&.hide {
display: none !important;
}
}
#modal {
max-width: 860px;
width: 90%;
min-height: 200px;
background-color: #333;
padding: unset;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
header {
height: 50px;
font-size: 30px;
line-height: 50px;
padding-left: 10px;
overflow: hidden;
background-color: #212121;
display: flex;
.headline {
flex-grow: 1;
}
.close {
margin-right: 10px;
font-size: 24px;
cursor: pointer;
&:hover {
color: #ff3232;
}
}
}
modal-content {
display: block;
max-height: calc(100vh - 200px);
overflow: auto;
}
modal-footer {
display: block;
}
}

57
raw/scss/_playlist.scss Normal file
View file

@ -0,0 +1,57 @@
playlist {
display: flex;
flex-direction: column;
div {
padding: unset;
position: unset;
}
.pagination {
display: flex;
justify-content: flex-end;
font-size: 1.5em;
padding: 5px;
.item {
cursor: pointer;
user-select: none;
border-radius: 5px;
margin: 0 3px;
&.inactive {
color: #aaa;
pointer-events: none;
cursor: not-allowed;
}
&:hover {
color: #006ea8;
}
}
}
.playlist-item {
display: flex;
padding: 5px;
border-bottom: 1px solid #dcdcdc;
cursor: pointer;
&-title {
margin-left: 10px;
padding: 5px;
display: flex;
align-items: center;
}
&-number {
padding: 5px 10px 5px 5px;
border-right: 1px solid #ff3232;
width: 50px;
}
&:hover {
background-color: rgba(0, 0, 0, .4);
}
}
}

View file

@ -40,4 +40,5 @@ div {
@import "gui";
@import "input";
@import "controls";
@import "controls";
@import "playlist";