Move to ES2015 classes. Makes HuesUI especially nice

master
William Toohey 9 years ago
parent f4f62d167b
commit 4145470d6a
  1. 4
      package.json
  2. 791
      src/js/HuesCanvas.js
  3. 1808
      src/js/HuesCore.js
  4. 2466
      src/js/HuesEditor.js
  5. 4
      src/js/HuesInfo.js
  6. 374
      src/js/HuesSettings.js
  7. 2159
      src/js/HuesUI.js
  8. 268
      src/js/HuesWindow.js
  9. 1510
      src/js/ResourceManager.js
  10. 851
      src/js/ResourcePack.js
  11. 930
      src/js/SoundManager.js

@ -20,7 +20,7 @@
}, },
"homepage": "https://github.com/mon/0x40-web#readme", "homepage": "https://github.com/mon/0x40-web#readme",
"devDependencies": { "devDependencies": {
"babel-preset-es2015": "^6.6.0", "babel-preset-es2015": "^6.9.0",
"del": "^2.2.0", "del": "^2.2.0",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.0", "gulp-autoprefixer": "^3.1.0",
@ -33,6 +33,6 @@
"gulp-plumber": "^1.1.0", "gulp-plumber": "^1.1.0",
"gulp-sourcemaps": "^1.6.0", "gulp-sourcemaps": "^1.6.0",
"gulp-uglify": "^1.5.3", "gulp-uglify": "^1.5.3",
"jshint": "^2.9.1" "jshint": "^2.9.2"
} }
} }

@ -25,149 +25,186 @@
/* Takes root element to attach to, and an audio context element for /* Takes root element to attach to, and an audio context element for
getting the current time with reasonable accuracy */ getting the current time with reasonable accuracy */
function HuesCanvas(root, audioContext, core) { class HuesCanvas {
this.audio = audioContext; constructor(root, audioContext, core) {
core.addEventListener("newimage", this.setImage.bind(this)); this.audio = audioContext;
core.addEventListener("newcolour", this.setColour.bind(this)); core.addEventListener("newimage", this.setImage.bind(this));
core.addEventListener("beat", this.beat.bind(this)); core.addEventListener("newcolour", this.setColour.bind(this));
core.addEventListener("invert", this.setInvert.bind(this)); core.addEventListener("beat", this.beat.bind(this));
core.addEventListener("settingsupdated", this.settingsUpdated.bind(this)); core.addEventListener("invert", this.setInvert.bind(this));
core.addEventListener("frame", this.animationLoop.bind(this)); core.addEventListener("settingsupdated", this.settingsUpdated.bind(this));
this.core = core; core.addEventListener("frame", this.animationLoop.bind(this));
this.core = core;
this.needsRedraw = false;
this.colour = 0xFFFFFF;
this.image = null;
this.smartAlign = true; // avoid string comparisons every frame
this.animTimeout = null;
this.animFrame = null;
this.lastBeat = 0;
// set later
this.blurDecay = null;
this.blurAmount = null;
this.blurIterations = null;
this.blurDelta = null;
this.blurAlpha = null;
// dynamic
this.blurStart = 0;
this.blurDistance = 0;
this.xBlur = false;
this.yBlur = false;
// trippy mode
this.trippyStart = [0, 0]; // x, y
this.trippyRadii = [0, 0]; // x, y
// force trippy mode
this.trippyOn = false;
this.trippyRadius = 0;
this.blackout = false;
this.blackoutColour = "#000"; // for the whiteout case we must store this
this.blackoutTimeout = null;
this.invert = false;
this.colourFade = false;
this.colourFadeStart=0;
this.colourFadeLength=0;
this.oldColour=0xFFFFFF;
this.newColour=0xFFFFFF;
this.blendMode = "hard-light";
// Chosen because they look decent
this.setBlurAmount("medium");
this.setBlurQuality("high");
this.setBlurDecay("fast");
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d");
this.canvas.width = 1280;
this.canvas.height = 720;
this.canvas.className = "hues-canvas";
root.appendChild(this.canvas);
this.offCanvas = document.createElement('canvas');
this.offContext = this.offCanvas.getContext('2d');
window.addEventListener('resize', this.resize.bind(this));
this.resize();
}
HuesCanvas.prototype.setInvert = function(invert) { this.needsRedraw = false;
this.invert = invert; this.colour = 0xFFFFFF;
this.needsRedraw = true; this.image = null;
}; this.smartAlign = true; // avoid string comparisons every frame
HuesCanvas.prototype.settingsUpdated = function() { this.animTimeout = null;
this.setSmartAlign(localStorage["smartAlign"]); this.animFrame = null;
this.setBlurAmount(localStorage["blurAmount"]); this.lastBeat = 0;
this.setBlurDecay(localStorage["blurDecay"]);
this.setBlurQuality(localStorage["blurQuality"]); // set later
this.trippyOn = localStorage["trippyMode"] == "on"; this.blurDecay = null;
}; this.blurAmount = null;
this.blurIterations = null;
HuesCanvas.prototype.resize = function() { this.blurDelta = null;
// height is max 720px, we expand width to suit this.blurAlpha = null;
let height = this.core.root.clientHeight; // dynamic
let ratio = this.core.root.clientWidth / height; this.blurStart = 0;
this.canvas.height = Math.min(height, 720); this.blurDistance = 0;
this.canvas.width = Math.ceil(this.canvas.height * ratio); this.xBlur = false;
this.offCanvas.height = this.canvas.height; this.yBlur = false;
this.offCanvas.width = this.canvas.width;
this.trippyRadius = Math.max(this.canvas.width, this.canvas.height) / 2; // trippy mode
this.needsRedraw = true; this.trippyStart = [0, 0]; // x, y
}; this.trippyRadii = [0, 0]; // x, y
// force trippy mode
HuesCanvas.prototype.redraw = function() { this.trippyOn = false;
let offset; // for centering/right/left align this.trippyRadius = 0;
let bOpacity;
let width = this.canvas.width; this.blackout = false;
let height = this.canvas.height; this.blackoutColour = "#000"; // for the whiteout case we must store this
this.blackoutTimeout = null;
let cTime = this.audio.currentTime;
// white BG for the hard light filter this.invert = false;
this.context.globalAlpha = 1;
this.context.globalCompositeOperation = "source-over"; this.colourFade = false;
if(this.blackout) { this.colourFadeStart=0;
// original is 3 frames at 30fps, this is close this.colourFadeLength=0;
bOpacity = (cTime - this.blackoutStart)*10; this.oldColour=0xFFFFFF;
if(bOpacity > 1) { // optimise the draw this.newColour=0xFFFFFF;
this.context.fillStyle = this.blackoutColour;
this.blendMode = "hard-light";
// Chosen because they look decent
this.setBlurAmount("medium");
this.setBlurQuality("high");
this.setBlurDecay("fast");
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext("2d");
this.canvas.width = 1280;
this.canvas.height = 720;
this.canvas.className = "hues-canvas";
root.appendChild(this.canvas);
this.offCanvas = document.createElement('canvas');
this.offContext = this.offCanvas.getContext('2d');
window.addEventListener('resize', this.resize.bind(this));
this.resize();
}
setInvert(invert) {
this.invert = invert;
this.needsRedraw = true;
}
settingsUpdated() {
this.setSmartAlign(localStorage["smartAlign"]);
this.setBlurAmount(localStorage["blurAmount"]);
this.setBlurDecay(localStorage["blurDecay"]);
this.setBlurQuality(localStorage["blurQuality"]);
this.trippyOn = localStorage["trippyMode"] == "on";
}
resize() {
// height is max 720px, we expand width to suit
let height = this.core.root.clientHeight;
let ratio = this.core.root.clientWidth / height;
this.canvas.height = Math.min(height, 720);
this.canvas.width = Math.ceil(this.canvas.height * ratio);
this.offCanvas.height = this.canvas.height;
this.offCanvas.width = this.canvas.width;
this.trippyRadius = Math.max(this.canvas.width, this.canvas.height) / 2;
this.needsRedraw = true;
}
redraw() {
let offset; // for centering/right/left align
let bOpacity;
let width = this.canvas.width;
let height = this.canvas.height;
let cTime = this.audio.currentTime;
// white BG for the hard light filter
this.context.globalAlpha = 1;
this.context.globalCompositeOperation = "source-over";
if(this.blackout) {
// original is 3 frames at 30fps, this is close
bOpacity = (cTime - this.blackoutStart)*10;
if(bOpacity > 1) { // optimise the draw
this.context.fillStyle = this.blackoutColour;
this.context.fillRect(0,0,width,height);
this.needsRedraw = false;
this.drawInvert();
return;
}
} else {
this.context.fillStyle = "#FFF";
this.context.fillRect(0,0,width,height); this.context.fillRect(0,0,width,height);
this.needsRedraw = false;
this.drawInvert();
return;
} }
} else {
this.context.fillStyle = "#FFF";
this.context.fillRect(0,0,width,height);
}
if(this.image && (this.image.bitmap || this.image.bitmaps)) { if(this.image && (this.image.bitmap || this.image.bitmaps)) {
let bitmap = this.image.animated ? let bitmap = this.image.animated ?
this.image.bitmaps[this.animFrame] : this.image.bitmap; this.image.bitmaps[this.animFrame] : this.image.bitmap;
let drawHeight = bitmap.height * (height / bitmap.height); let drawHeight = bitmap.height * (height / bitmap.height);
let drawWidth = (bitmap.width / bitmap.height) * drawHeight; let drawWidth = (bitmap.width / bitmap.height) * drawHeight;
if(this.smartAlign) { if(this.smartAlign) {
switch(this.image.align) { switch(this.image.align) {
case "left": case "left":
offset = 0; offset = 0;
break; break;
case "right": case "right":
offset = width - drawWidth; offset = width - drawWidth;
break; break;
default: default:
offset = width/2 - drawWidth/2; offset = width/2 - drawWidth/2;
break; break;
}
} else {
offset = width/2 - drawWidth/2;
} }
if(this.xBlur || this.yBlur) {
this.drawBlur(bitmap, offset, drawWidth, drawHeight);
}else {
this.context.globalAlpha = 1;
this.context.drawImage(bitmap, offset, 0, drawWidth, drawHeight);
}
}
if(this.trippyStart[0] || this.trippyStart[1]) {
this.drawTrippy(width, height);
} else { } else {
offset = width/2 - drawWidth/2; this.offContext.fillStyle = this.intToHex(this.colour);
this.offContext.fillRect(0,0,width,height);
} }
if(this.xBlur || this.yBlur) { this.context.globalAlpha = 0.7;
this.context.globalAlpha = this.blurAlpha; this.context.globalCompositeOperation = this.blendMode;
this.context.drawImage(this.offCanvas, 0, 0);
if(this.blackout) {
this.context.globalAlpha = bOpacity;
this.context.fillStyle = this.blackoutColour;
this.context.fillRect(0,0,width,height);
this.needsRedraw = true;
} else {
this.needsRedraw = false;
}
this.drawInvert();
}
drawInvert() {
if(this.invert) {
this.context.globalAlpha = 1;
this.context.globalCompositeOperation = "difference";
this.context.fillStyle = "#FFF";
this.context.fillRect(0,0,this.canvas.width,this.canvas.height);
} }
}
drawBlur(bitmap, offset, drawWidth, drawHeight) {
this.context.globalAlpha = this.blurAlpha;
if(this.xBlur) { if(this.xBlur) {
if(this.blurIterations < 0) { if(this.blurIterations < 0) {
this.context.globalAlpha = 1; this.context.globalAlpha = 1;
@ -188,13 +225,11 @@ HuesCanvas.prototype.redraw = function() {
this.context.drawImage(bitmap, offset, Math.floor(this.blurDistance * i), drawWidth, drawHeight); this.context.drawImage(bitmap, offset, Math.floor(this.blurDistance * i), drawWidth, drawHeight);
} }
} }
} else {
this.context.globalAlpha = 1;
this.context.drawImage(bitmap, offset, 0, drawWidth, drawHeight);
} }
} }
if(this.trippyStart[0] || this.trippyStart[1]) { // draws the correct trippy colour circles onto the offscreen canvas
drawTrippy(width, height) {
// x blur moves inwards from the corners, y comes out // x blur moves inwards from the corners, y comes out
// So the base colour is inverted for y, normal for x // So the base colour is inverted for y, normal for x
// Thus if the y start is more recent, we invert // Thus if the y start is more recent, we invert
@ -222,289 +257,265 @@ HuesCanvas.prototype.redraw = function() {
this.offContext.closePath(); this.offContext.closePath();
invert = !invert; invert = !invert;
} }
} else {
this.offContext.fillStyle = this.intToHex(this.colour);
this.offContext.fillRect(0,0,width,height);
} }
this.context.globalAlpha = 0.7;
this.context.globalCompositeOperation = this.blendMode;
this.context.drawImage(this.offCanvas, 0, 0);
if(this.blackout) {
this.context.globalAlpha = bOpacity;
this.context.fillStyle = this.blackoutColour;
this.context.fillRect(0,0,width,height);
this.needsRedraw = true;
} else {
this.needsRedraw = false;
}
this.drawInvert();
};
HuesCanvas.prototype.drawInvert = function() { /* Second fastest method from
if(this.invert) { http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript
this.context.globalAlpha = 1; It stil does millions of ops per second, and isn't ugly like the integer if/else */
this.context.globalCompositeOperation = "difference"; intToHex(num) {
this.context.fillStyle = "#FFF"; return '#' + ("00000"+num.toString(16)).slice(-6);
this.context.fillRect(0,0,this.canvas.width,this.canvas.height);
} }
};
animationLoop() {
/* Second fastest method from if (this.colourFade) {
http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript let delta = this.audio.currentTime - this.colourFadeStart;
It stil does millions of ops per second, and isn't ugly like the integer if/else */ let fadeVal = delta / this.colourFadeLength;
HuesCanvas.prototype.intToHex = function(num) { if (fadeVal >= 1) {
return '#' + ("00000"+num.toString(16)).slice(-6); this.stopFade();
}; this.colour = this.newColour;
} else {
HuesCanvas.prototype.animationLoop = function() { this.mixColours(fadeVal);
if (this.colourFade) { }
let delta = this.audio.currentTime - this.colourFadeStart; this.needsRedraw = true;
let fadeVal = delta / this.colourFadeLength;
if (fadeVal >= 1) {
this.stopFade();
this.colour = this.newColour;
} else {
this.mixColours(fadeVal);
} }
this.needsRedraw = true; if(this.blackoutTimeout && this.audio.currentTime > this.blackoutTimeout) {
} this.clearBlackout();
if(this.blackoutTimeout && this.audio.currentTime > this.blackoutTimeout) { }
this.clearBlackout(); if(this.image && this.image.animated){
} if(this.image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) {
if(this.image && this.image.animated){ let a = this.animFrame;
if(this.image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { this.syncAnim();
let a = this.animFrame; if(this.animFrame != a) {
this.syncAnim(); this.needsRedraw = true;
if(this.animFrame != a) { // If you change to a non-synced song, this needs to be reset
this.animTimeout = this.audio.currentTime;
}
} else if(this.animTimeout < this.audio.currentTime) {
this.animFrame++;
this.animFrame %= this.image.frameDurations.length;
// Don't rebase to current time otherwise we may lag
this.animTimeout += this.image.frameDurations[this.animFrame]/1000;
this.needsRedraw = true; this.needsRedraw = true;
// If you change to a non-synced song, this needs to be reset
this.animTimeout = this.audio.currentTime;
} }
} else if(this.animTimeout < this.audio.currentTime) { }
this.animFrame++; if(this.blurStart) {
this.animFrame %= this.image.frameDurations.length; // flash offsets blur gen by a frame
// Don't rebase to current time otherwise we may lag let delta = this.audio.currentTime - this.blurStart + (1/30);
this.animTimeout += this.image.frameDurations[this.animFrame]/1000; this.blurDistance = this.blurAmount * Math.exp(-this.blurDecay * delta);
// Update UI
let dist = this.blurDistance / this.blurAmount;
if(this.xBlur)
this.core.blurUpdated(dist, 0);
else
this.core.blurUpdated(0, dist);
}
if(this.trippyStart[0] || this.trippyStart[1]) {
for(let i = 0; i < 2; i++) {
this.trippyRadii[i] = Math.floor((this.audio.currentTime - this.trippyStart[i]) * this.trippyRadius) * 2;
if(this.trippyRadii[i] > this.trippyRadius) {
this.trippyStart[i] = 0;
this.trippyRadii[i] = 0;
continue;
}
// x comes from outside the window
if(i % 2 === 0) {
this.trippyRadii[i] = this.trippyRadius - this.trippyRadii[i];
}
}
this.needsRedraw = true; this.needsRedraw = true;
} }
if(this.blurStart && this.blurDistance < 1) {
this.core.blurUpdated(0, 0);
this.blurDistance = 0;
this.blurStart = 0;
this.xBlur = this.yBlur = false;
this.redraw();
} else if(this.blurStart) {
this.redraw();
} else if(this.needsRedraw){
this.redraw();
}
} }
if(this.blurStart) {
// flash offsets blur gen by a frame setImage(image) {
let delta = this.audio.currentTime - this.blurStart + (1/30); if(this.image == image) {
this.blurDistance = this.blurAmount * Math.exp(-this.blurDecay * delta); return;
// Update UI
let dist = this.blurDistance / this.blurAmount;
if(this.xBlur)
this.core.blurUpdated(dist, 0);
else
this.core.blurUpdated(0, dist);
}
if(this.trippyStart[0] || this.trippyStart[1]) {
for(let i = 0; i < 2; i++) {
this.trippyRadii[i] = Math.floor((this.audio.currentTime - this.trippyStart[i]) * this.trippyRadius) * 2;
if(this.trippyRadii[i] > this.trippyRadius) {
this.trippyStart[i] = 0;
this.trippyRadii[i] = 0;
continue;
}
// x comes from outside the window
if(i % 2 === 0) {
this.trippyRadii[i] = this.trippyRadius - this.trippyRadii[i];
}
} }
this.needsRedraw = true; this.needsRedraw = true;
this.image = image;
// Null images don't need anything interesting done to them
if(!image || (!image.bitmap && !image.bitmaps)) {
return;
}
if(image.animated) {
this.animBeat = null;
this.animFrame = 0;
this.animTimeout = this.audio.currentTime + image.frameDurations[0]/1000;
if(image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) {
this.syncAnim();
}
}
} }
if(this.blurStart && this.blurDistance < 1) { beat() {
this.core.blurUpdated(0, 0); this.lastBeat = this.audio.currentTime;
this.blurDistance = 0;
this.blurStart = 0;
this.xBlur = this.yBlur = false;
this.redraw();
} else if(this.blurStart) {
this.redraw();
} else if(this.needsRedraw){
this.redraw();
} }
};
HuesCanvas.prototype.setImage = function(image) { syncAnim() {
if(this.image == image) { let song = this.core.currentSong;
return; if(!song) { // fallback to default
return;
}
let index = this.core.beatIndex;
// When animation has more frames than song has beats, or part thereof
if(this.lastBeat && this.core.getBeatLength()) {
let interp = (this.audio.currentTime - this.lastBeat) / this.core.getBeatLength();
index += Math.min(interp, 1);
}
// This loops A-OK because the core's beatIndex never rolls over for a new loop
let beatLoc = (index / song.charsPerBeat) % this.image.beatsPerAnim;
let aLen = this.image.bitmaps.length;
this.animFrame = Math.floor(aLen * (beatLoc / this.image.beatsPerAnim));
if(this.image.syncOffset) {
this.animFrame += this.image.syncOffset;
}
// Because negative mods are different in JS
this.animFrame = ((this.animFrame % aLen) + aLen) % aLen;
} }
this.needsRedraw = true;
this.image = image; setColour(colour, isFade) {
// Null images don't need anything interesting done to them if(colour.c == this.colour) {
if(!image || (!image.bitmap && !image.bitmaps)) { return;
return; }
if(isFade) {
this.newColour = colour.c;
} else {
this.stopFade();
this.colour = colour.c;
}
this.needsRedraw = true;
} }
if(image.animated) {
this.animBeat = null; doBlackout(whiteout) {
this.animFrame = 0; if (typeof(whiteout)==='undefined') whiteout = false;
this.animTimeout = this.audio.currentTime + image.frameDurations[0]/1000; if(whiteout) {
if(image.beatsPerAnim && this.core.currentSong && this.core.currentSong.charsPerBeat) { this.blackoutColour = "#FFF";
this.syncAnim(); } else {
this.blackoutColour = "#000";
}
this.blackoutTimeout = 0; // indefinite
// Don't restart the blackout animation if we're already blacked out
if(!this.blackout) {
this.blackoutStart = this.audio.currentTime;
}
this.blackout = true;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.hide();
} }
} }
};
HuesCanvas.prototype.beat = function() { // for song changes
this.lastBeat = this.audio.currentTime; clearBlackout() {
}; this.blackout = false;
this.blackoutTimeout = 0;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.show();
}
}
HuesCanvas.prototype.syncAnim = function() { doShortBlackout(beatTime) {
let song = this.core.currentSong; this.doBlackout();
if(!song) { // fallback to default this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7;
return; // looks better if we go right to black
this.blackoutStart = 0;
} }
let index = this.core.beatIndex;
// When animation has more frames than song has beats, or part thereof doColourFade(length) {
if(this.lastBeat && this.core.getBeatLength()) { this.colourFade = true;
let interp = (this.audio.currentTime - this.lastBeat) / this.core.getBeatLength(); this.colourFadeLength = length;
index += Math.min(interp, 1); this.colourFadeStart = this.audio.currentTime;
this.oldColour = this.colour;
} }
// This loops A-OK because the core's beatIndex never rolls over for a new loop
let beatLoc = (index / song.charsPerBeat) % this.image.beatsPerAnim;
let aLen = this.image.bitmaps.length; stopFade() {
this.animFrame = Math.floor(aLen * (beatLoc / this.image.beatsPerAnim)); this.colourFade = false;
if(this.image.syncOffset) { this.colourFadeStart = 0;
this.animFrame += this.image.syncOffset; this.colourFadeLength = 0;
} }
// Because negative mods are different in JS
this.animFrame = ((this.animFrame % aLen) + aLen) % aLen;
};
HuesCanvas.prototype.setColour = function(colour, isFade) { mixColours(percent) {
if(colour.c == this.colour) { percent = Math.min(1, percent);
return; let oldR = this.oldColour >> 16 & 0xFF;
let oldG = this.oldColour >> 8 & 0xFF;
let oldB = this.oldColour & 0xFF;
let newR = this.newColour >> 16 & 0xFF;
let newG = this.newColour >> 8 & 0xFF;
let newB = this.newColour & 0xFF;
let mixR = oldR * (1 - percent) + newR * percent;
let mixG = oldG * (1 - percent) + newG * percent;
let mixB = oldB * (1 - percent) + newB * percent;
this.colour = mixR << 16 | mixG << 8 | mixB;
} }
if(isFade) {
this.newColour = colour.c; doXBlur() {
} else { this.blurStart = this.audio.currentTime;
this.stopFade(); if(this.trippyOn)
this.colour = colour.c; this.trippyStart[0] = this.blurStart;
this.blurDistance = this.blurAmount;
this.xBlur = true;
this.yBlur = false;
this.needsRedraw = true;
} }
this.needsRedraw = true;
}; doYBlur() {
this.blurStart = this.audio.currentTime;
HuesCanvas.prototype.doBlackout = function(whiteout) { if(this.trippyOn)
if (typeof(whiteout)==='undefined') whiteout = false; this.trippyStart[1] = this.blurStart;
if(whiteout) { this.blurDistance = this.blurAmount;
this.blackoutColour = "#FFF"; this.xBlur = false;
} else { this.yBlur = true;
this.blackoutColour = "#000"; this.needsRedraw = true;
} }
this.blackoutTimeout = 0; // indefinite
// Don't restart the blackout animation if we're already blacked out doTrippyX() {
if(!this.blackout) { let saveTrippy = this.trippyOn;
this.blackoutStart = this.audio.currentTime; // force trippy
this.trippyOn = true;
this.doXBlur();
this.trippyOn = saveTrippy;
} }
this.blackout = true;
this.needsRedraw = true; doTrippyY() {
if(localStorage["blackoutUI"] == "on") { let saveTrippy = this.trippyOn;
this.core.userInterface.hide(); // force trippy
this.trippyOn = true;
this.doYBlur();
this.trippyOn = saveTrippy;
} }
};
setBlurDecay(decay) {
// for song changes this.blurDecay = {"slow" : 7.8, "medium" : 14.1, "fast" : 20.8, "faster!" : 28.7}[decay];
HuesCanvas.prototype.clearBlackout = function() {
this.blackout = false;
this.blackoutTimeout = 0;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.show();
} }
};
setBlurQuality(quality) {
HuesCanvas.prototype.doShortBlackout = function(beatTime) { this.blurIterations = {"low" : -1, "medium" : 11, "high" : 19, "extreme" : 35}[quality];
this.doBlackout(); this.blurDelta = 1 / (this.blurIterations/2);
this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7; this.blurAlpha = 1 / (this.blurIterations/2);
// looks better if we go right to black }
this.blackoutStart = 0;
}; setBlurAmount(amount) {
this.blurAmount = {"low" : 48, "medium" : 96, "high" : 384}[amount];
HuesCanvas.prototype.doColourFade = function(length) { }
this.colourFade = true;
this.colourFadeLength = length; setSmartAlign(align) {
this.colourFadeStart = this.audio.currentTime; this.smartAlign = align == "on";
this.oldColour = this.colour; }
}; }
HuesCanvas.prototype.stopFade = function() {
this.colourFade = false;
this.colourFadeStart = 0;
this.colourFadeLength = 0;
};
HuesCanvas.prototype.mixColours = function(percent) {
percent = Math.min(1, percent);
let oldR = this.oldColour >> 16 & 0xFF;
let oldG = this.oldColour >> 8 & 0xFF;
let oldB = this.oldColour & 0xFF;
let newR = this.newColour >> 16 & 0xFF;
let newG = this.newColour >> 8 & 0xFF;
let newB = this.newColour & 0xFF;
let mixR = oldR * (1 - percent) + newR * percent;
let mixG = oldG * (1 - percent) + newG * percent;
let mixB = oldB * (1 - percent) + newB * percent;
this.colour = mixR << 16 | mixG << 8 | mixB;
};
HuesCanvas.prototype.doXBlur = function() {
this.blurStart = this.audio.currentTime;
if(this.trippyOn)
this.trippyStart[0] = this.blurStart;
this.blurDistance = this.blurAmount;
this.xBlur = true;
this.yBlur = false;
this.needsRedraw = true;
};
HuesCanvas.prototype.doYBlur = function() {
this.blurStart = this.audio.currentTime;
if(this.trippyOn)
this.trippyStart[1] = this.blurStart;
this.blurDistance = this.blurAmount;
this.xBlur = false;
this.yBlur = true;
this.needsRedraw = true;
};
HuesCanvas.prototype.doTrippyX = function() {
let saveTrippy = this.trippyOn;
// force trippy
this.trippyOn = true;
this.doXBlur();
this.trippyOn = saveTrippy;
};
HuesCanvas.prototype.doTrippyY = function() {
let saveTrippy = this.trippyOn;
// force trippy
this.trippyOn = true;
this.doYBlur();
this.trippyOn = saveTrippy;
};
HuesCanvas.prototype.setBlurDecay = function(decay) {
this.blurDecay = {"slow" : 7.8, "medium" : 14.1, "fast" : 20.8, "faster!" : 28.7}[decay];
};
HuesCanvas.prototype.setBlurQuality = function(quality) {
this.blurIterations = {"low" : -1, "medium" : 11, "high" : 19, "extreme" : 35}[quality];
this.blurDelta = 1 / (this.blurIterations/2);
this.blurAlpha = 1 / (this.blurIterations/2);
};
HuesCanvas.prototype.setBlurAmount = function(amount) {
this.blurAmount = {"low" : 48, "medium" : 96, "high" : 384}[amount];
};
HuesCanvas.prototype.setSmartAlign = function(align) {
this.smartAlign = align == "on";
};
window.HuesCanvas = HuesCanvas; window.HuesCanvas = HuesCanvas;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -26,7 +26,7 @@
/* HuesInfo.js populates the INFO tab in the Hues Window. /* HuesInfo.js populates the INFO tab in the Hues Window.
*/ */
let beatGlossary = [ const beatGlossary = [
"x Vertical blur (snare)", "x Vertical blur (snare)",
"o Horizontal blur (bass)", "o Horizontal blur (bass)",
"- No blur", "- No blur",
@ -44,7 +44,7 @@ let beatGlossary = [
"I Invert & change image" "I Invert & change image"
]; ];
let shortcuts = [ const shortcuts = [
"↑↓ Change song", "↑↓ Change song",
"←→ Change image", "←→ Change image",
"[N] Random song", "[N] Random song",

@ -26,7 +26,7 @@
/* If you're modifying settings for your hues, DON'T EDIT THIS /* If you're modifying settings for your hues, DON'T EDIT THIS
- Go to the HTML and edit the `defaults` object instead! - Go to the HTML and edit the `defaults` object instead!
*/ */
HuesSettings.prototype.defaultSettings = { const defaultSettings = {
// Location relative to root - where do the audio/zip workers live // Location relative to root - where do the audio/zip workers live
// This is required because Web Workers need an absolute path // This is required because Web Workers need an absolute path
workersPath : "lib/workers/", workersPath : "lib/workers/",
@ -80,7 +80,7 @@ HuesSettings.prototype.defaultSettings = {
}; };
// Don't get saved to localStorage // Don't get saved to localStorage
HuesSettings.prototype.ephemeralSettings = [ const ephemeralSettings = [
"load", "load",
"autoplay", "autoplay",
"overwriteLocal", "overwriteLocal",
@ -100,7 +100,7 @@ HuesSettings.prototype.ephemeralSettings = [
]; ];
// To dynamically build the UI like the cool guy I am // To dynamically build the UI like the cool guy I am
HuesSettings.prototype.settingsCategories = { const settingsCategories = {
"Functionality" : [ "Functionality" : [
"autoSong", "autoSong",
"autoSongShuffle", "autoSongShuffle",
@ -126,7 +126,7 @@ HuesSettings.prototype.settingsCategories = {
] ]
}; };
HuesSettings.prototype.settingsOptions = { const settingsOptions = {
smartAlign : { smartAlign : {
name : "Smart Align images", name : "Smart Align images",
options : ["off", "on"] options : ["off", "on"]
@ -218,223 +218,225 @@ HuesSettings.prototype.settingsOptions = {
} }
}; };
function HuesSettings(defaults) { class HuesSettings {
this.eventListeners = { constructor(defaults) {
/* callback updated() this.eventListeners = {
* /* callback updated()
* Called when settings are updated *
*/ * Called when settings are updated
updated : [] */
}; updated : []
};
this.hasUI = false; this.hasUI = false;
this.settingCheckboxes = {}; this.settingCheckboxes = {};
this.textCallbacks = []; this.textCallbacks = [];
this.visCallbacks = []; this.visCallbacks = [];
for(let attr in this.defaultSettings) { for(let attr in defaultSettings) {
if(this.defaultSettings.hasOwnProperty(attr)) { if(defaultSettings.hasOwnProperty(attr)) {
if(defaults[attr] === undefined) { if(defaults[attr] === undefined) {
defaults[attr] = this.defaultSettings[attr]; defaults[attr] = defaultSettings[attr];
}
// don't write to local if it's a temp settings
if(ephemeralSettings.indexOf(attr) != -1) {
continue;
}
if(defaults.overwriteLocal) {
localStorage[attr] = defaults[attr];
}
// populate defaults, ignoring current
if(localStorage[attr] === undefined) {
localStorage[attr] = defaults[attr];
}
} }
// don't write to local if it's a temp settings }
if(this.ephemeralSettings.indexOf(attr) != -1) {
continue;
}
if(defaults.overwriteLocal) {
localStorage[attr] = defaults[attr];
}
// populate defaults, ignoring current
if(localStorage[attr] === undefined) {
localStorage[attr] = defaults[attr];
}
}
}
this.defaults = defaults; this.defaults = defaults;
} }
HuesSettings.prototype.initUI = function(huesWin) { initUI(huesWin) {
let root = document.createElement("div"); let root = document.createElement("div");
root.className = "hues-options"; root.className = "hues-options";
// Don't make in every loop // Don't make in every loop
let intValidator = function(self, variable) { let intValidator = function(self, variable) {
this.value = this.value.replace(/\D/g,''); this.value = this.value.replace(/\D/g,'');
if(this.value === "" || this.value < 1) { if(this.value === "" || this.value < 1) {
this.value = ""; this.value = "";
return; return;
} }
localStorage[variable] = this.value; localStorage[variable] = this.value;
self.updateConditionals(); self.updateConditionals();
self.callEventListeners("updated"); self.callEventListeners("updated");
}; };
// To order things nicely // To order things nicely
for(let cat in this.settingsCategories) { for(let cat in settingsCategories) {
if(this.settingsCategories.hasOwnProperty(cat)) { if(settingsCategories.hasOwnProperty(cat)) {
let catContainer = document.createElement("div"); let catContainer = document.createElement("div");
catContainer.textContent = cat; catContainer.textContent = cat;
catContainer.className = "settings-category"; catContainer.className = "settings-category";
let cats = this.settingsCategories[cat]; let cats = settingsCategories[cat];
for(let i = 0; i < cats.length; i++) { for(let i = 0; i < cats.length; i++) {
let setName = cats[i]; let setName = cats[i];
let setContainer = document.createElement("div"); let setContainer = document.createElement("div");
let setting = this.settingsOptions[setName]; let setting = settingsOptions[setName];
setContainer.textContent = setting.name; setContainer.textContent = setting.name;
setContainer.className = "settings-individual"; setContainer.className = "settings-individual";
let buttonContainer = document.createElement("div"); let buttonContainer = document.createElement("div");
buttonContainer.className = "settings-buttons"; buttonContainer.className = "settings-buttons";
for(let j = 0; j < setting.options.length; j++) { for(let j = 0; j < setting.options.length; j++) {
let option = setting.options[j]; let option = setting.options[j];
if(typeof option === "string") { if(typeof option === "string") {
let checkbox = document.createElement("input"); let checkbox = document.createElement("input");
// Save checkbox so we can update UI stuff // Save checkbox so we can update UI stuff
this.settingCheckboxes[setName + "-" + option] = checkbox; this.settingCheckboxes[setName + "-" + option] = checkbox;
checkbox.className = "settings-checkbox"; checkbox.className = "settings-checkbox";
checkbox.type = "radio"; checkbox.type = "radio";
checkbox.value = option; checkbox.value = option;
let unique = 0; let unique = 0;
// Lets us have multiple hues on 1 page // Lets us have multiple hues on 1 page
let id = setName + "-" + option + "-"; let id = setName + "-" + option + "-";
while(document.getElementById(id + unique)) { while(document.getElementById(id + unique)) {
unique++; unique++;
} }
checkbox.name = setName + "-" + unique; checkbox.name = setName + "-" + unique;
checkbox.id = id + unique; checkbox.id = id + unique;
if(localStorage[setName] == option) { if(localStorage[setName] == option) {
checkbox.checked = true; checkbox.checked = true;
}
checkbox.onclick = function(self) {
self.set(setName, this.value);
}.bind(checkbox, this);
buttonContainer.appendChild(checkbox);
// So we can style this nicely
let label = document.createElement("label");
label.className = "settings-label";
label.htmlFor = checkbox.id;
label.textContent = option.toUpperCase();
buttonContainer.appendChild(label);
} else { // special option
if(option.type == "varText") {
let text = document.createElement("span");
text.textContent = option.text();
buttonContainer.appendChild(text);
this.textCallbacks.push({func:option.text, element:text});
} else if(option.type == "input") {
let input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "settings-input";
input.value = localStorage[option.variable];
// TODO: support more than just positive ints when the need arises
if(option.inputType == "int") {
input.oninput = intValidator.bind(input, this, option.variable);
} }
input.autofocus = false; checkbox.onclick = function(self) {
buttonContainer.appendChild(input); self.set(setName, this.value);
if(option.visiblity) { }.bind(checkbox, this);
this.visCallbacks.push({func:option.visiblity, element:input}); buttonContainer.appendChild(checkbox);
input.style.visibility = option.visiblity() ? "visible" : "hidden"; // So we can style this nicely
let label = document.createElement("label");
label.className = "settings-label";
label.htmlFor = checkbox.id;
label.textContent = option.toUpperCase();
buttonContainer.appendChild(label);
} else { // special option
if(option.type == "varText") {
let text = document.createElement("span");
text.textContent = option.text();
buttonContainer.appendChild(text);
this.textCallbacks.push({func:option.text, element:text});
} else if(option.type == "input") {
let input = document.createElement("input");
input.setAttribute("type", "text");
input.className = "settings-input";
input.value = localStorage[option.variable];
// TODO: support more than just positive ints when the need arises
if(option.inputType == "int") {
input.oninput = intValidator.bind(input, this, option.variable);
}
input.autofocus = false;
buttonContainer.appendChild(input);
if(option.visiblity) {
this.visCallbacks.push({func:option.visiblity, element:input});
input.style.visibility = option.visiblity() ? "visible" : "hidden";
}
} }
} }
}
}
setContainer.appendChild(buttonContainer);
catContainer.appendChild(setContainer);
} }
setContainer.appendChild(buttonContainer); root.appendChild(catContainer);
catContainer.appendChild(setContainer);
} }
root.appendChild(catContainer);
} }
huesWin.addTab("OPTIONS", root);
this.hasUI = true;
} }
huesWin.addTab("OPTIONS", root);
this.hasUI = true;
};
HuesSettings.prototype.get = function(setting) { get(setting) {
if(this.defaults.hasOwnProperty(setting)) { if(this.defaults.hasOwnProperty(setting)) {
if(this.ephemeralSettings.indexOf(setting) != -1) { if(ephemeralSettings.indexOf(setting) != -1) {
return this.defaults[setting]; return this.defaults[setting];
} else {
return localStorage[setting];
}
} else { } else {
return localStorage[setting]; console.log("WARNING: Attempted to fetch invalid setting:", setting);
return null;
} }
} else {
console.log("WARNING: Attempted to fetch invalid setting:", setting);
return null;
} }
};
// Set a named index to its named value, returns false if name doesn't exist // Set a named index to its named value, returns false if name doesn't exist
HuesSettings.prototype.set = function(setting, value) { set(setting, value) {
value = value.toLowerCase(); value = value.toLowerCase();
let opt = this.settingsOptions[setting]; let opt = settingsOptions[setting];
if(!opt || opt.options.indexOf(value) == -1) { if(!opt || opt.options.indexOf(value) == -1) {
console.log(value, "is not a valid value for", setting); console.log(value, "is not a valid value for", setting);
return false; return false;
}
// for updating the UI selection
try {
this.settingCheckboxes[setting + "-" + value].checked = true;
} catch(e) {}
localStorage[setting] = value;
this.updateConditionals();
this.callEventListeners("updated");
return true;
} }
// for updating the UI selection
try {
this.settingCheckboxes[setting + "-" + value].checked = true;
} catch(e) {}
localStorage[setting] = value;
this.updateConditionals();
this.callEventListeners("updated");
return true;
};
HuesSettings.prototype.updateConditionals = function() { updateConditionals() {
// update any conditionally formatted settings text // update any conditionally formatted settings text
for(let i = 0; i < this.textCallbacks.length; i++) { for(let i = 0; i < this.textCallbacks.length; i++) {
let text = this.textCallbacks[i]; let text = this.textCallbacks[i];
text.element.textContent = text.func(); text.element.textContent = text.func();
} }
for(let i = 0; i < this.visCallbacks.length; i++) { for(let i = 0; i < this.visCallbacks.length; i++) {
let callback = this.visCallbacks[i]; let callback = this.visCallbacks[i];
callback.element.style.visibility = callback.func() ? "visible" : "hidden"; callback.element.style.visibility = callback.func() ? "visible" : "hidden";
}
} }
};
// Note: This is not defaults as per defaultSettings, but those merged with // Note: This is not defaults as per defaultSettings, but those merged with
// the defaults given in the initialiser // the defaults given in the initialiser
HuesSettings.prototype.setDefaults = function() { setDefaults() {
for(let attr in this.defaults) { for(let attr in this.defaults) {
if(this.defaults.hasOwnProperty(attr)) { if(this.defaults.hasOwnProperty(attr)) {
if(this.ephemeralSettings.indexOf(attr) != -1) { if(ephemeralSettings.indexOf(attr) != -1) {
continue; continue;
}
localStorage[attr] = this.defaults[attr];
} }
localStorage[attr] = this.defaults[attr];
} }
} }
};
HuesSettings.prototype.callEventListeners = function(ev) { callEventListeners(ev) {
let args = Array.prototype.slice.call(arguments, 1); let args = Array.prototype.slice.call(arguments, 1);
this.eventListeners[ev].forEach(function(callback) { this.eventListeners[ev].forEach(function(callback) {
callback.apply(null, args); callback.apply(null, args);
}); });
}; }
HuesSettings.prototype.addEventListener = function(ev, callback) { addEventListener(ev, callback) {
ev = ev.toLowerCase(); ev = ev.toLowerCase();
if (typeof(this.eventListeners[ev]) !== "undefined") { if (typeof(this.eventListeners[ev]) !== "undefined") {
this.eventListeners[ev].push(callback); this.eventListeners[ev].push(callback);
} else { } else {
throw Error("Unknown event: " + ev); throw Error("Unknown event: " + ev);
}
} }
};
HuesSettings.prototype.removeEventListener = function(ev, callback) { removeEventListener(ev, callback) {
ev = ev.toLowerCase(); ev = ev.toLowerCase();
if (typeof(this.eventListeners[ev]) !== "undefined") { if (typeof(this.eventListeners[ev]) !== "undefined") {
this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) {
return (a !== callback); return (a !== callback);
}); });
} else { } else {
throw Error("Unknown event: " + ev); throw Error("Unknown event: " + ev);
}
} }
}; }
window.HuesSettings = HuesSettings; window.HuesSettings = HuesSettings;

File diff suppressed because it is too large Load Diff

@ -22,149 +22,151 @@
(function(window, document) { (function(window, document) {
"use strict"; "use strict";
function HuesWindow(root, defaults) { class HuesWindow {
this.eventListeners = { constructor(root, defaults) {
/* callback windowshown(shown) this.eventListeners = {
* /* callback windowshown(shown)
* When the window is shown, hidden or toggled this fires. *
* 'shown' is true if the window was made visible, false otherwise * When the window is shown, hidden or toggled this fires.
*/ * 'shown' is true if the window was made visible, false otherwise
windowshown : [], */
/* callback tabselected(tabName) windowshown : [],
* /* callback tabselected(tabName)
* The name of the tab that was selected *
*/ * The name of the tab that was selected
tabselected : [] */
}; tabselected : []
};
this.hasUI = defaults.enableWindow;
this.hasUI = defaults.enableWindow;
if(!this.hasUI)
return; if(!this.hasUI)
return;
this.window = document.createElement("div");
this.window.className = "hues-win-helper"; this.window = document.createElement("div");
root.appendChild(this.window); this.window.className = "hues-win-helper";
root.appendChild(this.window);
let actualWindow = document.createElement("div");
actualWindow.className = "hues-win"; let actualWindow = document.createElement("div");
this.window.appendChild(actualWindow); actualWindow.className = "hues-win";
this.window.appendChild(actualWindow);
let closeButton = document.createElement("div");
closeButton.className = "hues-win__closebtn"; let closeButton = document.createElement("div");
closeButton.onclick = this.hide.bind(this); closeButton.className = "hues-win__closebtn";
actualWindow.appendChild(closeButton); closeButton.onclick = this.hide.bind(this);
actualWindow.appendChild(closeButton);
this.tabContainer = document.createElement("div");
this.tabContainer.className = "hues-win__tabs"; this.tabContainer = document.createElement("div");
actualWindow.appendChild(this.tabContainer); this.tabContainer.className = "hues-win__tabs";
actualWindow.appendChild(this.tabContainer);
this.contentContainer = document.createElement("div");
this.contentContainer.className = "hues-win__content"; this.contentContainer = document.createElement("div");
actualWindow.appendChild(this.contentContainer); this.contentContainer.className = "hues-win__content";
actualWindow.appendChild(this.contentContainer);
this.contents = [];
this.tabs = []; this.contents = [];
this.tabNames = []; this.tabs = [];
this.tabNames = [];
if(defaults.showWindow) {
this.show(); if(defaults.showWindow) {
} else { this.show();
this.hide(); } else {
this.hide();
}
} }
}
HuesWindow.prototype.addTab = function(tabName, tabContent) { addTab(tabName, tabContent) {
if(!this.hasUI) if(!this.hasUI)
return; return;
let label = document.createElement("div"); let label = document.createElement("div");
label.textContent = tabName; label.textContent = tabName;
label.className = "tab-label"; label.className = "tab-label";
label.onclick = this.selectTab.bind(this, tabName); label.onclick = this.selectTab.bind(this, tabName);
this.tabContainer.appendChild(label); this.tabContainer.appendChild(label);
this.tabs.push(label); this.tabs.push(label);
this.tabNames.push(tabName); this.tabNames.push(tabName);
let content = document.createElement("div"); let content = document.createElement("div");
content.className = "tab-content"; content.className = "tab-content";
content.appendChild(tabContent); content.appendChild(tabContent);
this.contentContainer.appendChild(content); this.contentContainer.appendChild(content);
this.contents.push(content); this.contents.push(content);
};
HuesWindow.prototype.selectTab = function(tabName, dontShowWin) {
if(!this.hasUI)
return;
if(!dontShowWin) {
this.show();
} }
for(let i = 0; i < this.tabNames.length; i++) {
let name = this.tabNames[i]; selectTab(tabName, dontShowWin) {
if(tabName.toLowerCase() == name.toLowerCase()) { if(!this.hasUI)
this.contents[i].classList.add("tab-content--active"); return;
this.tabs[i].classList.add("tab-label--active"); if(!dontShowWin) {
this.callEventListeners("tabselected", name); this.show();
} else { }
this.contents[i].classList.remove("tab-content--active"); for(let i = 0; i < this.tabNames.length; i++) {
this.tabs[i].classList.remove("tab-label--active"); let name = this.tabNames[i];
if(tabName.toLowerCase() == name.toLowerCase()) {
this.contents[i].classList.add("tab-content--active");
this.tabs[i].classList.add("tab-label--active");
this.callEventListeners("tabselected", name);
} else {
this.contents[i].classList.remove("tab-content--active");
this.tabs[i].classList.remove("tab-label--active");
}
} }
} }
};
hide() {
HuesWindow.prototype.hide = function() { if(!this.hasUI)
if(!this.hasUI) return;
return;
this.window.classList.add("hidden");
this.window.classList.add("hidden"); this.callEventListeners("windowshown", false);
this.callEventListeners("windowshown", false);
};
HuesWindow.prototype.show = function() {
if(!this.hasUI)
return;
this.window.classList.remove("hidden");
this.callEventListeners("windowshown", true);
};
HuesWindow.prototype.toggle = function() {
if(!this.hasUI)
return;
if(this.window.classList.contains("hidden")) {
this.show();
} else {
this.hide();
} }
};
show() {
HuesWindow.prototype.callEventListeners = function(ev) { if(!this.hasUI)
let args = Array.prototype.slice.call(arguments, 1); return;
this.eventListeners[ev].forEach(function(callback) {
callback.apply(null, args); this.window.classList.remove("hidden");
}); this.callEventListeners("windowshown", true);
};
HuesWindow.prototype.addEventListener = function(ev, callback) {
ev = ev.toLowerCase();
if (typeof(this.eventListeners[ev]) !== "undefined") {
this.eventListeners[ev].push(callback);
} else {
throw Error("Unknown event: " + ev);
} }
};
HuesWindow.prototype.removeEventListener = function(ev, callback) { toggle() {
ev = ev.toLowerCase(); if(!this.hasUI)
if (typeof(this.eventListeners[ev]) !== "undefined") { return;
this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) { if(this.window.classList.contains("hidden")) {
return (a !== callback); this.show();
} else {
this.hide();
}
}
callEventListeners(ev) {
let args = Array.prototype.slice.call(arguments, 1);
this.eventListeners[ev].forEach(function(callback) {
callback.apply(null, args);
}); });
} else {
throw Error("Unknown event: " + ev);
} }
};
addEventListener(ev, callback) {
ev = ev.toLowerCase();
if (typeof(this.eventListeners[ev]) !== "undefined") {
this.eventListeners[ev].push(callback);
} else {
throw Error("Unknown event: " + ev);
}
}
removeEventListener(ev, callback) {
ev = ev.toLowerCase();
if (typeof(this.eventListeners[ev]) !== "undefined") {
this.eventListeners[ev] = this.eventListeners[ev].filter(function(a) {
return (a !== callback);
});
} else {
throw Error("Unknown event: " + ev);
}
}
}
window.HuesWindow = HuesWindow; window.HuesWindow = HuesWindow;

File diff suppressed because it is too large Load Diff

@ -22,513 +22,514 @@
(function(window, document) { (function(window, document) {
"use strict"; "use strict";
let debugConsole = false; const debugConsole = false;
function debug() { function debug() {
if(debugConsole) { if(debugConsole) {
console.log.apply(window.console, arguments); console.log.apply(window.console, arguments);
} }
} }
function Respack() { const audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i");
this.songs = []; const imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i");
this.songQueue = []; const animRegex = new RegExp("(.*?)_\\d+$");
this.images = [];
this.imageQueue = [];
this.name = "<no name>"; class Respack {
this.author = "<unknown>"; constructor() {
this.description = "<no description>"; this.songs = [];
this.link = null; this.songQueue = [];
this.images = [];
this.imageQueue = [];
this.size = -1; this.name = "<no name>";
this.downloaded = -1; this.author = "<unknown>";
this.enabled = true; this.description = "<no description>";
this.link = null;
this._xmlQueue = []; this.size = -1;
this.downloaded = -1;
this.enabled = true;
this.totalFiles = -1; this._xmlQueue = [];
// For zip parsing progress events
this.progressCallback = null;
this.filesToLoad = 0;
this.filesLoaded = 0;
this.loadedFromURL = false;
}
Respack.prototype.audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i"); this.totalFiles = -1;
Respack.prototype.imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i");
Respack.prototype.animRegex = new RegExp("(.*?)_\\d+$");
Respack.prototype.updateProgress = function(override) { // For zip parsing progress events
if(this.progressCallback) { this.progressCallback = null;
let percent = this.filesLoaded / this.filesToLoad; this.filesToLoad = 0;
if(this.loadedFromURL) { this.filesLoaded = 0;
percent = (percent / 2) + 0.5; this.loadedFromURL = false;
}
this.progressCallback(typeof override === "number" ? override : percent, this);
} }
};
Respack.prototype.loadFromURL = function(url, progress) { updateProgress(override) {
this.loadedFromURL = true; if(this.progressCallback) {
if(progress) { let percent = this.filesLoaded / this.filesToLoad;
this.progressCallback = progress; if(this.loadedFromURL) {
percent = (percent / 2) + 0.5;
}
this.progressCallback(typeof override === "number" ? override : percent, this);
}
} }
return this.getBlob(url) loadFromURL(url, progress) {
.then(response => { this.loadedFromURL = true;
return this.loadFromBlob(response); if(progress) {
}); this.progressCallback = progress;
}; }
Respack.prototype.getBlob = function(url, progress) { return this.getBlob(url)
if(progress) { .then(response => {
this.progressCallback = progress; return this.loadFromBlob(response);
});
} }
return new Promise ((resolve, reject) => {
let req = new XMLHttpRequest(); getBlob(url, progress) {
req.open('GET', url, true); if(progress) {
req.responseType = 'blob'; this.progressCallback = progress;
req.onload = () => { }
if(req.status == 200) { return new Promise ((resolve, reject) => {
resolve(req.response); let req = new XMLHttpRequest();
} else { req.open('GET', url, true);
req.responseType = 'blob';
req.onload = () => {
if(req.status == 200) {
resolve(req.response);
} else {
reject(Error(req.status + ": Could not fetch respack at " + url));
}
};
req.onerror = function() {
reject(Error(req.status + ": Could not fetch respack at " + url)); reject(Error(req.status + ": Could not fetch respack at " + url));
} };
}; req.onprogress = event => {
req.onerror = function() { if (event.lengthComputable) {
reject(Error(req.status + ": Could not fetch respack at " + url)); this.size = event.total;
}; this.downloaded = event.loaded;
req.onprogress = event => { let percent = event.loaded / event.total;
if (event.lengthComputable) { if(this.progressCallback) {
this.size = event.total; this.progressCallback(percent / 2, this); // because of processing too
this.downloaded = event.loaded; }
let percent = event.loaded / event.total;
if(this.progressCallback) {
this.progressCallback(percent / 2, this); // because of processing too
} }
};
req.send();
}).catch(error => {
// Infinitely more user friendly than the error Same Origin gives
if(error.code == 1012) {
throw Error("Respack at URL " + url + " is restricted. Check CORS.");
} else {
throw error;
} }
}; });
req.send(); }
}).catch(error => {
// Infinitely more user friendly than the error Same Origin gives
if(error.code == 1012) {
throw Error("Respack at URL " + url + " is restricted. Check CORS.");
} else {
throw error;
}
});
};
Respack.prototype.loadFromBlob = function(blob, progress) { loadFromBlob(blob, progress) {
if(progress) { if(progress) {
this.progressCallback = progress; this.progressCallback = progress;
}
// We don't get progress events for loading the zip, set 0 progress
this.updateProgress(this.loadedFromURL ? 0.5 : 0);
return new Promise((resolve, reject) => {
this.size = blob.size;
let file = new zip.fs.FS();
file.importBlob(blob,
() => {
resolve(file);
},
error => { // failure
reject(Error("Respack error:", error.toString()));
}
);
}).then(zip => {
return this.parseZip(zip);
}).then(() => {
return this;
});
} }
// We don't get progress events for loading the zip, set 0 progress
this.updateProgress(this.loadedFromURL ? 0.5 : 0);
return new Promise((resolve, reject) => {
this.size = blob.size;
let file = new zip.fs.FS();
file.importBlob(blob,
() => {
resolve(file);
},
error => { // failure
reject(Error("Respack error:", error.toString()));
}
);
}).then(zip => {
return this.parseZip(zip);
}).then(() => {
return this;
});
};
Respack.prototype.parseZip = function(zip) { parseZip(zip) {
let entries = zip.entries; let entries = zip.entries;
this.totalFiles = 0; this.totalFiles = 0;
// Progress events // Progress events
this.filesToLoad = 0; this.filesToLoad = 0;
this.filesLoaded = 0; this.filesLoaded = 0;
// Get everything started // Get everything started
for(let i = 0; i < entries.length; i++) { for(let i = 0; i < entries.length; i++) {
if(!entries[i].directory && entries[i].name) { if(!entries[i].directory && entries[i].name) {
this.totalFiles++; this.totalFiles++;
this.parseFile(entries[i]); this.parseFile(entries[i]);
}
} }
return this.parseSongQueue()
.then(() => {
return this.parseImageQueue();
}).then(() => {
return this.parseXML();
}).then(() => {
// Cleanup
this._xmlQueue = [];
console.log("Loaded", this.name, "successfully with", this.songs.length,
"songs and", this.images.length, "images.");
});
} }
return this.parseSongQueue() parseFile(file) {
.then(() => { let name = file.name;
return this.parseImageQueue(); if (name.match(audioExtensions)) {
}).then(() => { this.songQueue.push(this.parseSong(file));
return this.parseXML(); this.filesToLoad++;
}).then(() => { } else if (name.match(imageExtensions)) {
// Cleanup this.imageQueue.push(this.parseImage(file));
this._xmlQueue = []; this.filesToLoad++;
console.log("Loaded", this.name, "successfully with", this.songs.length, } else if(name.toLowerCase().endsWith(".xml")){
"songs and", this.images.length, "images."); this._xmlQueue.push(this.loadXML(file));
}); }
}; }
Respack.prototype.parseFile = function(file) { parseSong(file) {
let name = file.name; let name = file.name.replace(audioExtensions, "");
if (name.match(this.audioExtensions)) { debug("parsing song: " + name);
this.songQueue.push(this.parseSong(file)); if (this.containsSong(name)) {
this.filesToLoad++; let oldSong = this.getSong(name);
} else if (name.match(this.imageExtensions)) { debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name);
this.imageQueue.push(this.parseImage(file)); } else {
this.filesToLoad++; let newSong = {"name":name,
"title":null,
"rhythm":null,
"source":null,
//"crc":this.quickCRC(file), TODO
"sound":null,
"enabled":true,
"filename":file.name,
"charsPerBeat": null};
let extension = file.name.split('.').pop().toLowerCase();
let mime = "";
switch(extension) {
case "mp3":
mime = "audio/mpeg3";
break;
case "ogg":
mime = "audio/ogg";
break;
case "wav":
mime = "audio/wav";
break;
default:
mime = "application/octet-stream";
}
this.songs.push(newSong);
return new Promise((resolve, reject) => {
file.getBlob(mime, sound => {
resolve(sound);
});
}).then(blob => {
return new Promise((resolve, reject) => {
// Because blobs are crap
let fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsArrayBuffer(blob);
});
}).then(sound => {
newSong.sound = sound;
this.filesLoaded++;
this.updateProgress();
});
}
} }
else if(name.toLowerCase().endsWith(".xml")){
this._xmlQueue.push(this.loadXML(file)); parseSongQueue() {
return this.songQueue.reduce((sequence, songPromise) => {
return sequence.then(() => {
// Maintain order
return songPromise;
});
}, Promise.resolve());
}
parseImage(file) {
let match;
let name = file.name.replace(imageExtensions, "");
let img;
// Animation
if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) {
img = this.getImage(match[1]);
if(!img) { // make a fresh one
img = {"name":match[1],
"fullname":match[1],
"align":"center",
//"crc":this.quickCRC(file),
"bitmaps":[],
"frameDurations":[33],
"source":null,
"enabled":true,
"animated":true,
"beatsPerAnim": null};
this.images.push(img);
}
// Normal image
} else if (!this.containsImage(name)) {
img = {"name":name,
"fullname":name,
"bitmap":null,
"align":"center",
//"crc":this.quickCRC(file),
"source":null,
"enabled":true,
"filename":file.name,
"animated":false};
this.images.push(img);
} else {
let existing = this.getImage(name);
console.log("WARNING: Image", name, "already exists! Conflict with", file.name, "and", existing.name);
return;
}
return this.loadImage(file, img);
} }
};
Respack.prototype.parseSong = function(file) { loadImage(imgFile, imageObj) {
let name = file.name.replace(this.audioExtensions, ""); let extension = imgFile.name.split('.').pop().toLowerCase();
debug("parsing song: " + name);
if (this.containsSong(name)) {
let oldSong = this.getSong(name);
debug("WARNING: Song", name, "already exists! Conflict with", name, "and", oldSong.name);
} else {
let newSong = {"name":name,
"title":null,
"rhythm":null,
"source":null,
//"crc":this.quickCRC(file), TODO
"sound":null,
"enabled":true,
"filename":file.name,
"charsPerBeat": null};
let extension = file.name.split('.').pop().toLowerCase();
let mime = ""; let mime = "";
switch(extension) { switch(extension) {
case "mp3": case "png":
mime = "audio/mpeg3"; mime = "image/png";
break; break;
case "ogg": case "gif":
mime = "audio/ogg"; mime = "image/gif";
break; break;
case "wav": case "jpg":
mime = "audio/wav"; case "jpeg":
mime = "image/jpeg";
break; break;
default: default:
mime = "application/octet-stream"; mime = "application/octet-stream";
} }
this.songs.push(newSong);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
file.getBlob(mime, sound => { imgFile.getData64URI(mime, resolve);
resolve(sound); }).then(bitmap => {
});
}).then(blob => {
return new Promise((resolve, reject) => {
// Because blobs are crap
let fr = new FileReader();
fr.onload = () => {
resolve(fr.result);
};
fr.readAsArrayBuffer(blob);
});
}).then(sound => {
newSong.sound = sound;
this.filesLoaded++; this.filesLoaded++;
this.updateProgress(); this.updateProgress();
return {bitmap: bitmap, img: imageObj};
}); });
} }
};
Respack.prototype.parseSongQueue = function() {
return this.songQueue.reduce((sequence, songPromise) => {
return sequence.then(() => {
// Maintain order
return songPromise;
});
}, Promise.resolve());
};
Respack.prototype.parseImage = function(file) {
let match;
let name = file.name.replace(this.imageExtensions, "");
let img;
// Animation
if((match = name.match(new RegExp("^(.*)_(\\d+)$")))) {
img = this.getImage(match[1]);
if(!img) { // make a fresh one
img = {"name":match[1],
"fullname":match[1],
"align":"center",
//"crc":this.quickCRC(file),
"bitmaps":[],
"frameDurations":[33],
"source":null,
"enabled":true,
"animated":true,
"beatsPerAnim": null};
this.images.push(img);
}
// Normal image
} else if (!this.containsImage(name)) {
img = {"name":name,
"fullname":name,
"bitmap":null,
"align":"center",
//"crc":this.quickCRC(file),
"source":null,
"enabled":true,
"filename":file.name,
"animated":false};
this.images.push(img);
} else {
let existing = this.getImage(name);
console.log("WARNING: Image", name, "already exists! Conflict with", file.name, "and", existing.name);
return;
}
return this.loadImage(file, img); parseImageQueue() {
}; return this.imageQueue.reduce((sequence, imagePromise) => {
return sequence.then(() => {
Respack.prototype.loadImage = function(imgFile, imageObj) { // Maintain order
let extension = imgFile.name.split('.').pop().toLowerCase(); return imagePromise;
let mime = ""; }).then(response => {
switch(extension) { // Don't crash if the respack had duplicate images
case "png": if(!response)
mime = "image/png"; return;
break; let newImg = new Image();
case "gif": newImg.src = response.bitmap;
mime = "image/gif"; if (response.img.animated) {
break; response.img.bitmaps.push(newImg);
case "jpg": } else {
case "jpeg": response.img.bitmap = newImg;
mime = "image/jpeg"; }
break; });
default: }, Promise.resolve());
mime = "application/octet-stream";
} }
return new Promise((resolve, reject) => {
imgFile.getData64URI(mime, resolve);
}).then(bitmap => {
this.filesLoaded++;
this.updateProgress();
return {bitmap: bitmap, img: imageObj};
});
};
Respack.prototype.parseImageQueue = function() {
return this.imageQueue.reduce((sequence, imagePromise) => {
return sequence.then(() => {
// Maintain order
return imagePromise;
}).then(response => {
// Don't crash if the respack had duplicate images
if(!response)
return;
let newImg = new Image();
newImg.src = response.bitmap;
if (response.img.animated) {
response.img.bitmaps.push(newImg);
} else {
response.img.bitmap = newImg;
}
});
}, Promise.resolve());
};
Respack.prototype.loadXML = function(file) {
return new Promise((resolve, reject) => {
file.getText(text => {
//XML parser will complain about a bare '&', but some respacks use &amp
text = text.replace(/&amp;/g, '&');
text = text.replace(/&/g, '&amp;');
let parser = new DOMParser();
let dom = parser.parseFromString(text, "text/xml");
resolve(dom);
});
});
};
Respack.prototype.parseXML = function() { loadXML(file) {
for(let i = 0; i < this._xmlQueue.length; i++) { return new Promise((resolve, reject) => {
this._xmlQueue[i] = this._xmlQueue[i].then(dom => { file.getText(text => {
switch(dom.documentElement.nodeName) { //XML parser will complain about a bare '&', but some respacks use &amp
case "songs": text = text.replace(/&amp;/g, '&');
if(this.songs.length > 0) text = text.replace(/&/g, '&amp;');
this.parseSongFile(dom); let parser = new DOMParser();
break; let dom = parser.parseFromString(text, "text/xml");
case "images": resolve(dom);
if(this.images.length > 0) });
this.parseImageFile(dom);
break;
case "info":
this.parseInfoFile(dom);
break;
default:
console.log("XML found with no songs, images or info");
break;
}
}); });
} }
return Promise.all(this._xmlQueue);
};
// Save some chars parseXML() {
Element.prototype.getTag = function(tag, def) { for(let i = 0; i < this._xmlQueue.length; i++) {
let t = this.getElementsByTagName(tag)[0]; this._xmlQueue[i] = this._xmlQueue[i].then(dom => {
return t ? t.textContent : (def ? def : null); switch(dom.documentElement.nodeName) {
}; case "songs":
if(this.songs.length > 0)
this.parseSongFile(dom);
break;
case "images":
if(this.images.length > 0)
this.parseImageFile(dom);
break;
case "info":
this.parseInfoFile(dom);
break;
default:
console.log("XML found with no songs, images or info");
break;
}
});
}
return Promise.all(this._xmlQueue);
}
Respack.prototype.parseSongFile = function(dom) { parseSongFile(dom) {
debug(" - Parsing songFile"); debug(" - Parsing songFile");
let newSongs = []; let newSongs = [];
let el = dom.documentElement.firstElementChild; let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) { for(; el; el = el.nextElementSibling) {
let song = this.getSong(el.attributes[0].value); let song = this.getSong(el.attributes[0].value);
if(song) { if(song) {
song.title = el.getTag("title"); song.title = el.getTag("title");
if(!song.title) { if(!song.title) {
song.title = "<no name>"; song.title = "<no name>";
debug(" WARNING!", song.name, "has no title!"); debug(" WARNING!", song.name, "has no title!");
} }
song.rhythm = el.getTag("rhythm"); song.rhythm = el.getTag("rhythm");
if(!song.rhythm) { if(!song.rhythm) {
song.rhythm = "..no..rhythm.."; song.rhythm = "..no..rhythm..";
debug(" WARNING!!", song.name, "has no rhythm!!"); debug(" WARNING!!", song.name, "has no rhythm!!");
} }
song.buildupName = el.getTag("buildup"); song.buildupName = el.getTag("buildup");
if(song.buildupName) { if(song.buildupName) {
debug(" Finding a buildup '" + song.buildupName + "' for ", song.name); debug(" Finding a buildup '" + song.buildupName + "' for ", song.name);
let build = this.getSong(song.buildupName); let build = this.getSong(song.buildupName);
if(build) { if(build) {
song.buildup = build.sound; song.buildup = build.sound;
song.buildupPlayed = false; song.buildupPlayed = false;
// get rid of the junk // get rid of the junk
this.songs.splice(this.songs.indexOf(build), 1); this.songs.splice(this.songs.indexOf(build), 1);
} else { } else {
debug(" WARNING!", "Didn't find a buildup '" + song.buildupName + "'!"); debug(" WARNING!", "Didn't find a buildup '" + song.buildupName + "'!");
}
} }
}
song.buildupRhythm = el.getTag("buildupRhythm"); song.buildupRhythm = el.getTag("buildupRhythm");
song.independentBuild = el.getTag("independentBuild"); song.independentBuild = el.getTag("independentBuild");
song.source = el.getTag("source"); song.source = el.getTag("source");
song.charsPerBeat = parseFloat(el.getTag("charsPerBeat")); song.charsPerBeat = parseFloat(el.getTag("charsPerBeat"));
// Because PackShit breaks everything // Because PackShit breaks everything
if(this.name == "PackShit") { if(this.name == "PackShit") {
song.forceTrim = true; song.forceTrim = true;
}
newSongs.push(song);
debug(" [I] " + song.name, ": '" + song.title + "' added to songs");
} else {
debug(" WARNING!", "songs.xml: <song> element", i + 1,
"- no song '" + el.attributes[0].value + "' found");
} }
newSongs.push(song);
debug(" [I] " + song.name, ": '" + song.title + "' added to songs");
} else {
debug(" WARNING!", "songs.xml: <song> element", i + 1,
"- no song '" + el.attributes[0].value + "' found");
} }
} for(let i = 0; i < this.songs.length; i++) {
for(let i = 0; i < this.songs.length; i++) { if(newSongs.indexOf(this.songs[i]) == -1) {
if(newSongs.indexOf(this.songs[i]) == -1) { debug(" WARNING!", "We have a file for", this.songs[i].name, "but no information for it");
debug(" WARNING!", "We have a file for", this.songs[i].name, "but no information for it"); }
} }
this.songs = newSongs;
} }
this.songs = newSongs;
};
Respack.prototype.parseInfoFile = function(dom) { parseInfoFile(dom) {
debug(" - Parsing infoFile"); debug(" - Parsing infoFile");
let info = dom.documentElement; let info = dom.documentElement;
// self reference strings to avoid changing strings twice in future // self reference strings to avoid changing strings twice in future
this.name = info.getTag("name", this.name); this.name = info.getTag("name", this.name);
this.author = info.getTag("author", this.author); this.author = info.getTag("author", this.author);
this.description = info.getTag("description", this.description); this.description = info.getTag("description", this.description);
this.link = info.getTag("link", this.link); this.link = info.getTag("link", this.link);
}; }
Respack.prototype.parseImageFile = function(dom) { parseImageFile(dom) {
debug(" - Parsing imagefile"); debug(" - Parsing imagefile");
let newImages = []; let newImages = [];
let el = dom.documentElement.firstElementChild; let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) { for(; el; el = el.nextElementSibling) {
let image = this.getImage(el.attributes[0].value); let image = this.getImage(el.attributes[0].value);
if(image) { if(image) {
image.fullname = el.getTag("fullname"); image.fullname = el.getTag("fullname");
if(!image.fullname) { if(!image.fullname) {
debug(" WARNING!", image.name, "has no full name!"); debug(" WARNING!", image.name, "has no full name!");
} }
image.source = el.getTag("source"); image.source = el.getTag("source");
// self reference defaults to avoid changing strings twice in future // self reference defaults to avoid changing strings twice in future
image.align = el.getTag("align", image.align); image.align = el.getTag("align", image.align);
image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim")); image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim"));
image.syncOffset = parseFloat(el.getTag("syncOffset")); image.syncOffset = parseFloat(el.getTag("syncOffset"));
let frameDur = el.getTag("frameDuration"); let frameDur = el.getTag("frameDuration");
if(frameDur) { if(frameDur) {
image.frameDurations = []; image.frameDurations = [];
let strSplit = frameDur.split(","); let strSplit = frameDur.split(",");
for(let j = 0; j < strSplit.length; j++) { for(let j = 0; j < strSplit.length; j++) {
image.frameDurations.push(parseInt(strSplit[j])); image.frameDurations.push(parseInt(strSplit[j]));
}
while (image.frameDurations.length < image.bitmaps.length) {
image.frameDurations.push(image.frameDurations[image.frameDurations.length - 1]);
}
debug("Frame durations:", image.frameDurations);
} }
while (image.frameDurations.length < image.bitmaps.length) { debug(" [I] " + image.name, ":", image.fullname, "added to images");
image.frameDurations.push(image.frameDurations[image.frameDurations.length - 1]); if (image.bitmap || image.bitmaps) {
newImages.push(image);
} }
debug("Frame durations:", image.frameDurations); else {
debug(" WARNING!!", "Image", image.name, "has no bitmap nor animation frames!");
}
} else {
debug(" WARNING!", "images.xml: no image '" + el.attributes[0].value + "' found");
} }
debug(" [I] " + image.name, ":", image.fullname, "added to images"); }
if (image.bitmap || image.bitmaps) { for(let i = 0; i < this.images.length; i++) {
let image = this.images[i];
// Add all images with no info
if(newImages.indexOf(image) == -1) {
newImages.push(image); newImages.push(image);
} }
else {
debug(" WARNING!!", "Image", image.name, "has no bitmap nor animation frames!");
}
} else {
debug(" WARNING!", "images.xml: no image '" + el.attributes[0].value + "' found");
}
}
for(let i = 0; i < this.images.length; i++) {
let image = this.images[i];
// Add all images with no info
if(newImages.indexOf(image) == -1) {
newImages.push(image);
} }
newImages.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
this.images = newImages;
} }
newImages.sort(function(a, b) {
return a.name.localeCompare(b.name);
});
this.images = newImages;
};
Respack.prototype.containsSong = function(name) { containsSong(name) {
return this.getSong(name) !== null; return this.getSong(name) !== null;
}; }
Respack.prototype.containsImage = function(name) { containsImage(name) {
return this.getImage(name) !== null; return this.getImage(name) !== null;
}; }
Respack.prototype.getSong = function(name) { getSong(name) {
for(let i = 0; i < this.songs.length; i++) { for(let i = 0; i < this.songs.length; i++) {
if (name == this.songs[i].name) { if (name == this.songs[i].name) {
return this.songs[i]; return this.songs[i];
}
} }
return null;
} }
return null;
};
Respack.prototype.getImage = function(name) { getImage(name) {
for(let i = 0; i < this.images.length; i++) { for(let i = 0; i < this.images.length; i++) {
if (name == this.images[i].name) { if (name == this.images[i].name) {
return this.images[i]; return this.images[i];
}
} }
return null;
} }
return null; }
};
window.Respack = Respack; window.Respack = Respack;
// Save some chars
Element.prototype.getTag = function(tag, def) {
let t = this.getElementsByTagName(tag)[0];
return t ? t.textContent : (def ? def : null);
};
})(window, document); })(window, document);

@ -22,531 +22,533 @@
(function(window, document) { (function(window, document) {
"use strict"; "use strict";
function SoundManager(core) { class SoundManager {
this.core = core; constructor(core) {
this.playing = false; this.core = core;
this.playbackRate = 1; this.playing = false;
this.song = null; this.playbackRate = 1;
this.song = null;
this.initPromise = null;
this.lockedPromise = null;
this.locked = true;
/* Lower level audio and timing info */
this.context = null; // Audio context, Web Audio API
this.oggSupport = false;
this.buildSource = null;
this.loopSource = null;
this.buildup = null;
this.loop = null;
this.startTime = 0; // File start time - 0 is loop start, not build start
this.buildLength = 0;
this.loopLength = 0; // For calculating beat lengths
// Volume
this.gainNode = null;
this.mute = false;
this.lastVol = 1;
// Visualiser
this.vReady = false;
this.vBars = 0;
this.vTotalBars = 0;
this.splitter = null;
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.linBins = 0;
this.logBins = 0;
this.maxBinLin = 0;
}
SoundManager.prototype.init = function() { this.initPromise = null;
if(!this.initPromise) { this.lockedPromise = null;
this.initPromise = new Promise((resolve, reject) => { this.locked = true;
// Check Web Audio API Support
try { /* Lower level audio and timing info */
// More info at http://caniuse.com/#feat=audio-api this.context = null; // Audio context, Web Audio API
window.AudioContext = window.AudioContext || window.webkitAudioContext; this.oggSupport = false;
// These don't always exist this.buildSource = null;
AudioContext.prototype.suspend = AudioContext.prototype.suspend || (() => {return Promise.resolve();}); this.loopSource = null;
AudioContext.prototype.resume = AudioContext.prototype.resume || (() => {return Promise.resolve();}); this.buildup = null;
this.loop = null;
this.context = new window.AudioContext(); this.startTime = 0; // File start time - 0 is loop start, not build start
this.gainNode = this.context.createGain(); this.buildLength = 0;
this.gainNode.connect(this.context.destination); this.loopLength = 0; // For calculating beat lengths
} catch(e) {
reject(Error("Web Audio API not supported in this browser.")); // Volume
return; this.gainNode = null;
} this.mute = false;
resolve(); this.lastVol = 1;
}).then(() => {
// check for .ogg support - if not, we'll have to load the ogg decoder // Visualiser
return new Promise((resolve, reject) => { this.vReady = false;
this.context.decodeAudioData(miniOgg, success => { this.vBars = 0;
this.oggSupport = true; this.vTotalBars = 0;
resolve(); this.splitter = null;
}, error => { this.analysers = [];
this.oggSupport = false; this.analyserArrays = [];
resolve(); this.logArrays = [];
}); this.binCutoffs = [];
}); this.linBins = 0;
}).then(() => { this.logBins = 0;
return new Promise((resolve, reject) => { this.maxBinLin = 0;
// See if our audio decoder is working }
let audioWorker;
init() {
if(!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
// Check Web Audio API Support
try { try {
audioWorker = this.createWorker(); // More info at http://caniuse.com/#feat=audio-api
window.AudioContext = window.AudioContext || window.webkitAudioContext;
// These don't always exist
AudioContext.prototype.suspend = AudioContext.prototype.suspend || (() => {return Promise.resolve();});
AudioContext.prototype.resume = AudioContext.prototype.resume || (() => {return Promise.resolve();});
this.context = new window.AudioContext();
this.gainNode = this.context.createGain();
this.gainNode.connect(this.context.destination);
} catch(e) { } catch(e) {
console.log(e); reject(Error("Web Audio API not supported in this browser."));
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
return; return;
} }
let pingListener = event => { resolve();
audioWorker.terminate(); }).then(() => {
resolve(); // check for .ogg support - if not, we'll have to load the ogg decoder
}; return new Promise((resolve, reject) => {
audioWorker.addEventListener('message', pingListener, false); this.context.decodeAudioData(miniOgg, success => {
audioWorker.addEventListener('error', () => { this.oggSupport = true;
reject(Error("Audio Worker cannot be started - correct path set in defaults?")); resolve();
}, false); }, error => {
audioWorker.postMessage({ping:true, ogg:this.oggSupport}); this.oggSupport = false;
resolve();
});
});
}).then(() => {
return new Promise((resolve, reject) => {
// See if our audio decoder is working
let audioWorker;
try {
audioWorker = this.createWorker();
} catch(e) {
console.log(e);
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
return;
}
let pingListener = event => {
audioWorker.terminate();
resolve();
};
audioWorker.addEventListener('message', pingListener, false);
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
}, false);
audioWorker.postMessage({ping:true, ogg:this.oggSupport});
});
}).then(() => {
this.locked = this.context.state != "running";
}); });
}).then(() => { }
this.locked = this.context.state != "running"; return this.initPromise;
});
} }
return this.initPromise;
};
SoundManager.prototype.unlock = function() { unlock() {
if(this.lockedPromise) { if(this.lockedPromise) {
return this.lockedPromise;
}
this.lockedPromise = new Promise((resolve, reject) => {
// iOS and other some mobile browsers - unlock the context as
// it starts in a suspended state
let unlocker = () => {
// create empty buffer
let buffer = this.context.createBuffer(1, 1, 22050);
let source = this.context.createBufferSource();
source.buffer = buffer;
// connect to output (your speakers)
source.connect( this.context.destination);
// play the file
source.start(0);
window.removeEventListener('touchend', unlocker);
window.removeEventListener('click', unlocker);
this.core.clearMessage();
resolve();
};
window.addEventListener('touchend', unlocker, false);
window.addEventListener('click', unlocker, false);
});
return this.lockedPromise; return this.lockedPromise;
} }
this.lockedPromise = new Promise((resolve, reject) => {
// iOS and other some mobile browsers - unlock the context as playSong(song, playBuild, forcePlay) {
// it starts in a suspended state let p = Promise.resolve();
let unlocker = () => { // Editor forces play on audio updates
// create empty buffer if(this.song == song && !forcePlay) {
let buffer = this.context.createBuffer(1, 1, 22050); return p;
let source = this.context.createBufferSource(); }
source.buffer = buffer; this.stop();
this.song = song;
// connect to output (your speakers) if(!song || (!song.sound)) { // null song
source.connect( this.context.destination); return p;
}
// play the file
source.start(0); // if there's a fadeout happening from AutoSong, kill it
this.gainNode.gain.cancelScheduledValues(0);
window.removeEventListener('touchend', unlocker); // Reset original volume
window.removeEventListener('click', unlocker); this.setVolume(this.lastVol);
this.core.clearMessage(); if(this.mute) {
resolve(); this.setMute(true);
}; }
window.addEventListener('touchend', unlocker, false);
window.addEventListener('click', unlocker, false); p = p.then(() => {
}); return this.loadSong(song);
return this.lockedPromise; }).then(buffers => {
}; // To prevent race condition if you press "next" twice fast
if(song != this.song) {
SoundManager.prototype.playSong = function(song, playBuild, forcePlay) { return Promise.reject("Song changed between load and play - this message can be ignored");
let p = Promise.resolve(); }
// Editor forces play on audio updates
if(this.song == song && !forcePlay) { this.buildup = buffers.buildup;
this.buildLength = this.buildup ? this.buildup.duration : 0;
this.loop = buffers.loop;
this.loopLength = this.loop.duration;
// This fixes sync issues on Firefox and slow machines.
return this.context.suspend();
}).then(() => {
if(playBuild) {
this.seek(-this.buildLength, true);
} else {
this.seek(0, true);
}
return this.context.resume();
}).then(() => {
this.playing = true;
});
return p; return p;
} }
this.stop();
this.song = song; stop(dontDeleteBuffers) {
if(!song || (!song.sound)) { // null song if (this.playing) {
return p; if(this.buildSource) {
this.buildSource.stop(0);
this.buildSource.disconnect();
this.buildSource = null;
if(!dontDeleteBuffers)
this.buildup = null;
}
// arg required for mobile webkit
this.loopSource.stop(0);
// TODO needed?
this.loopSource.disconnect();
this.loopSource = null;
if(!dontDeleteBuffers)
this.loop = null;
this.vReady = false;
this.playing = false;
this.startTime = 0;
}
} }
// if there's a fadeout happening from AutoSong, kill it setRate(rate) {
this.gainNode.gain.cancelScheduledValues(0); // Double speed is more than enough. Famous last words?
// Reset original volume rate = Math.max(Math.min(rate, 2), 0.25);
this.setVolume(this.lastVol);
if(this.mute) { let time = this.clampedTime();
this.setMute(true); this.playbackRate = rate;
this.seek(time);
} }
p = p.then(() => { seek(time, noPlayingUpdate) {
return this.loadSong(song); if(!this.song) {
}).then(buffers => { return;
// To prevent race condition if you press "next" twice fast
if(song != this.song) {
return Promise.reject("Song changed between load and play - this message can be ignored");
} }
//console.log("Seeking to " + time);
// Clamp the blighter
time = Math.min(Math.max(time, -this.buildLength), this.loopLength);
this.buildup = buffers.buildup; this.stop(true);
this.buildLength = this.buildup ? this.buildup.duration : 0;
this.loop = buffers.loop;
this.loopLength = this.loop.duration;
// This fixes sync issues on Firefox and slow machines. if(!this.loop) {
return this.context.suspend(); return;
}).then(() => {
if(playBuild) {
this.seek(-this.buildLength, true);
} else {
this.seek(0, true);
} }
return this.context.resume(); this.loopSource = this.context.createBufferSource();
}).then(() => { this.loopSource.buffer = this.loop;
this.playing = true; this.loopSource.playbackRate.value = this.playbackRate;
}); this.loopSource.loop = true;
return p; this.loopSource.loopStart = 0;
}; this.loopSource.loopEnd = this.loopLength;
this.loopSource.connect(this.gainNode);
if(time < 0 && this.buildup) {
this.buildSource = this.context.createBufferSource();
this.buildSource.buffer = this.buildup;
this.buildSource.playbackRate.value = this.playbackRate;
this.buildSource.connect(this.gainNode);
this.buildSource.start(0, this.buildLength + time);
this.loopSource.start(this.context.currentTime - (time / this.playbackRate));
} else {
this.loopSource.start(0, time);
}
SoundManager.prototype.stop = function(dontDeleteBuffers) { this.startTime = this.context.currentTime - (time / this.playbackRate);
if (this.playing) { if(!noPlayingUpdate) {
if(this.buildSource) { this.playing = true;
this.buildSource.stop(0);
this.buildSource.disconnect();
this.buildSource = null;
if(!dontDeleteBuffers)
this.buildup = null;
} }
// arg required for mobile webkit this.initVisualiser();
this.loopSource.stop(0); this.core.recalcBeatIndex();
// TODO needed?
this.loopSource.disconnect();
this.loopSource = null;
if(!dontDeleteBuffers)
this.loop = null;
this.vReady = false;
this.playing = false;
this.startTime = 0;
} }
};
SoundManager.prototype.setRate = function(rate) { // In seconds, relative to the loop start
// Double speed is more than enough. Famous last words? currentTime() {
rate = Math.max(Math.min(rate, 2), 0.25); if(!this.playing) {
return 0;
}
return (this.context.currentTime - this.startTime) * this.playbackRate;
}
let time = this.clampedTime(); clampedTime() {
this.playbackRate = rate; let time = this.currentTime();
this.seek(time);
};
SoundManager.prototype.seek = function(time, noPlayingUpdate) { if(time > 0) {
if(!this.song) { time %= this.loopLength;
return; }
return time;
} }
//console.log("Seeking to " + time);
// Clamp the blighter
time = Math.min(Math.max(time, -this.buildLength), this.loopLength);
this.stop(true); loadSong(song) {
if(song._loadPromise) {
/* Caused when moving back/forwards rapidly.
The sound is still loading. We reject this promise, and the already
running decode will finish and resolve instead.
NOTE: If anything but playSong calls loadSong, this idea is broken. */
return Promise.reject("Song changed between load and play - this message can be ignored");
}
if(!this.loop) { let buffers = {loop: null, buildup: null};
return;
}
this.loopSource = this.context.createBufferSource(); let promises = [this.loadBuffer(song, "sound").then(buffer => {
this.loopSource.buffer = this.loop; buffers.loop = buffer;
this.loopSource.playbackRate.value = this.playbackRate; })];
this.loopSource.loop = true; if(song.buildup) {
this.loopSource.loopStart = 0; promises.push(this.loadBuffer(song, "buildup").then(buffer => {
this.loopSource.loopEnd = this.loopLength; buffers.buildup = buffer;
this.loopSource.connect(this.gainNode); }));
} else {
if(time < 0 && this.buildup) { this.buildLength = 0;
this.buildSource = this.context.createBufferSource(); }
this.buildSource.buffer = this.buildup; song._loadPromise = Promise.all(promises)
this.buildSource.playbackRate.value = this.playbackRate; .then(() => {
this.buildSource.connect(this.gainNode); song._loadPromise = null;
this.buildSource.start(0, this.buildLength + time); return buffers;
this.loopSource.start(this.context.currentTime - (time / this.playbackRate)); });
} else { return song._loadPromise;
this.loopSource.start(0, time);
} }
this.startTime = this.context.currentTime - (time / this.playbackRate); loadBuffer(song, soundName) {
if(!noPlayingUpdate) { let buffer = song[soundName];
this.playing = true;
}
this.initVisualiser();
this.core.recalcBeatIndex();
};
// In seconds, relative to the loop start
SoundManager.prototype.currentTime = function() {
if(!this.playing) {
return 0;
}
return (this.context.currentTime - this.startTime) * this.playbackRate;
};
SoundManager.prototype.clampedTime = function() { // Is this an ogg file?
let time = this.currentTime(); let view = new Uint8Array(buffer);
// Signature for ogg file: OggS
if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) {
// As we don't control decodeAudioData, we cannot do fast transfers and must copy
let backup = buffer.slice(0);
return new Promise((resolve, reject) => {
this.context.decodeAudioData(buffer, result => {
resolve(result);
}, error => {
reject(Error("decodeAudioData failed to load track"));
});
}).then(result => {
// restore copied buffer
song[soundName] = backup;
return result;
});
} else { // Use our JS decoder
return new Promise((resolve, reject) => {
let audioWorker = this.createWorker();
if(time > 0) { audioWorker.addEventListener('error', () => {
time %= this.loopLength; reject(Error("Audio Worker failed to convert track"));
} }, false);
return time;
};
SoundManager.prototype.loadSong = function(song) {
if(song._loadPromise) {
/* Caused when moving back/forwards rapidly.
The sound is still loading. We reject this promise, and the already
running decode will finish and resolve instead.
NOTE: If anything but playSong calls loadSong, this idea is broken. */
return Promise.reject("Song changed between load and play - this message can be ignored");
}
let buffers = {loop: null, buildup: null}; audioWorker.addEventListener('message', e => {
let decoded = e.data;
audioWorker.terminate();
let promises = [this.loadBuffer(song, "sound").then(buffer => { // restore transferred buffer
buffers.loop = buffer; song[soundName] = decoded.arrayBuffer;
})]; if(decoded.error) {
if(song.buildup) { reject(new Error(decoded.error));
promises.push(this.loadBuffer(song, "buildup").then(buffer => { return;
buffers.buildup = buffer; }
})); // Convert to real audio buffer
} else { let audio = this.audioBufFromRaw(decoded.rawAudio);
this.buildLength = 0; resolve(audio);
} }, false);
song._loadPromise = Promise.all(promises)
.then(() => { // transfer the buffer to save time
song._loadPromise = null; audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]);
return buffers;
});
return song._loadPromise;
};
SoundManager.prototype.loadBuffer = function(song, soundName) {
let buffer = song[soundName];
// Is this an ogg file?
let view = new Uint8Array(buffer);
// Signature for ogg file: OggS
if(this.oggSupport && view[0] == 0x4F && view[1] == 0x67 && view[2] == 0x67 && view[3] == 0x53) {
// As we don't control decodeAudioData, we cannot do fast transfers and must copy
let backup = buffer.slice(0);
return new Promise((resolve, reject) => {
this.context.decodeAudioData(buffer, result => {
resolve(result);
}, error => {
reject(Error("decodeAudioData failed to load track"));
}); });
}).then(result => { }
// restore copied buffer
song[soundName] = backup;
return result;
});
} else { // Use our JS decoder
return new Promise((resolve, reject) => {
let audioWorker = this.createWorker();
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker failed to convert track"));
}, false);
audioWorker.addEventListener('message', e => {
let decoded = e.data;
audioWorker.terminate();
// restore transferred buffer
song[soundName] = decoded.arrayBuffer;
if(decoded.error) {
reject(new Error(decoded.error));
return;
}
// Convert to real audio buffer
let audio = this.audioBufFromRaw(decoded.rawAudio);
resolve(audio);
}, false);
// transfer the buffer to save time
audioWorker.postMessage({buffer: buffer, ogg: this.oggSupport}, [buffer]);
});
}
};
// Converts continuous PCM array to Web Audio API friendly format
SoundManager.prototype.audioBufFromRaw = function(raw) {
let buffer = raw.array;
let channels = raw.channels;
let samples = buffer.length/channels;
let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate);
for(let i = 0; i < channels; i++) {
// Offset is in bytes, length is in elements
let channel = new Float32Array(buffer.buffer , i * samples * 4, samples);
// Most browsers
if(typeof audioBuf.copyToChannel === "function") {
audioBuf.copyToChannel(channel, i, 0);
} else { // Safari, Edge sometimes
audioBuf.getChannelData(i).set(channel);
}
} }
return audioBuf;
};
SoundManager.prototype.createWorker = function() { // Converts continuous PCM array to Web Audio API friendly format
return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js'); audioBufFromRaw(raw) {
}; let buffer = raw.array;
let channels = raw.channels;
SoundManager.prototype.initVisualiser = function(bars) { let samples = buffer.length/channels;
// When restarting the visualiser let audioBuf = this.context.createBuffer(channels, samples, raw.sampleRate);
if(!bars) { for(let i = 0; i < channels; i++) {
bars = this.vTotalBars; // Offset is in bytes, length is in elements
} let channel = new Float32Array(buffer.buffer , i * samples * 4, samples);
this.vReady = false; // Most browsers
this.vTotalBars = bars; if(typeof audioBuf.copyToChannel === "function") {
for(let i = 0; i < this.analysers.length; i++) { audioBuf.copyToChannel(channel, i, 0);
this.analysers[i].disconnect(); } else { // Safari, Edge sometimes
audioBuf.getChannelData(i).set(channel);
}
}
return audioBuf;
} }
if(this.splitter) {
this.splitter.disconnect(); createWorker() {
this.splitter = null; return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js');
} }
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.linBins = 0; initVisualiser(bars) {
this.logBins = 0; // When restarting the visualiser
this.maxBinLin = 0; if(!bars) {
bars = this.vTotalBars;
}
this.vReady = false;
this.vTotalBars = bars;
for(let i = 0; i < this.analysers.length; i++) {
this.analysers[i].disconnect();
}
if(this.splitter) {
this.splitter.disconnect();
this.splitter = null;
}
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.attachVisualiser(); this.linBins = 0;
}; this.logBins = 0;
this.maxBinLin = 0;
SoundManager.prototype.attachVisualiser = function() { this.attachVisualiser();
if(!this.playing || this.vReady) {
return;
} }
// Get our info from the loop attachVisualiser() {
let channels = this.loopSource.channelCount; if(!this.playing || this.vReady) {
// In case channel counts change, this is changed each time return;
this.splitter = this.context.createChannelSplitter(channels);
// Connect to the gainNode so we get buildup stuff too
this.loopSource.connect(this.splitter);
if(this.buildSource) {
this.buildSource.connect(this.splitter);
}
// Split display up into each channel
this.vBars = Math.floor(this.vTotalBars/channels);
for(let i = 0; i < channels; i++) {
let analyser = this.context.createAnalyser();
// big fft buffers are new-ish
try {
analyser.fftSize = 8192;
} catch(err) {
analyser.fftSize = 2048;
} }
// Chosen because they look nice, no maths behind it
analyser.smoothingTimeConstant = 0.6; // Get our info from the loop
analyser.minDecibels = -70; let channels = this.loopSource.channelCount;
analyser.maxDecibels = -25; // In case channel counts change, this is changed each time
this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount)); this.splitter = this.context.createChannelSplitter(channels);
analyser.getByteTimeDomainData(this.analyserArrays[i]); // Connect to the gainNode so we get buildup stuff too
this.splitter.connect(analyser, i); this.loopSource.connect(this.splitter);
this.analysers.push(analyser); if(this.buildSource) {
this.logArrays.push(new Uint8Array(this.vBars)); this.buildSource.connect(this.splitter);
} }
let binCount = this.analysers[0].frequencyBinCount; // Split display up into each channel
let binWidth = this.loopSource.buffer.sampleRate / binCount; this.vBars = Math.floor(this.vTotalBars/channels);
// first 2kHz are linear
this.maxBinLin = Math.floor(2000/binWidth); for(let i = 0; i < channels; i++) {
// Don't stretch the first 2kHz, it looks awful let analyser = this.context.createAnalyser();
this.linBins = Math.min(this.maxBinLin, Math.floor(this.vBars/2)); // big fft buffers are new-ish
// Only go up to 22KHz try {
let maxBinLog = Math.floor(22000/binWidth); analyser.fftSize = 8192;
let logBins = this.vBars - this.linBins; } catch(err) {
analyser.fftSize = 2048;
let logLow = Math.log2(2000); }
let logDiff = Math.log2(22000) - logLow; // Chosen because they look nice, no maths behind it
for(let i = 0; i < logBins; i++) { analyser.smoothingTimeConstant = 0.6;
let cutoff = i * (logDiff/logBins) + logLow; analyser.minDecibels = -70;
let freqCutoff = Math.pow(2, cutoff); analyser.maxDecibels = -25;
let binCutoff = Math.floor(freqCutoff / binWidth); this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount));
this.binCutoffs.push(binCutoff); analyser.getByteTimeDomainData(this.analyserArrays[i]);
this.splitter.connect(analyser, i);
this.analysers.push(analyser);
this.logArrays.push(new Uint8Array(this.vBars));
}
let binCount = this.analysers[0].frequencyBinCount;
let binWidth = this.loopSource.buffer.sampleRate / binCount;
// first 2kHz are linear
this.maxBinLin = Math.floor(2000/binWidth);
// Don't stretch the first 2kHz, it looks awful
this.linBins = Math.min(this.maxBinLin, Math.floor(this.vBars/2));
// Only go up to 22KHz
let maxBinLog = Math.floor(22000/binWidth);
let logBins = this.vBars - this.linBins;
let logLow = Math.log2(2000);
let logDiff = Math.log2(22000) - logLow;
for(let i = 0; i < logBins; i++) {
let cutoff = i * (logDiff/logBins) + logLow;
let freqCutoff = Math.pow(2, cutoff);
let binCutoff = Math.floor(freqCutoff / binWidth);
this.binCutoffs.push(binCutoff);
}
this.vReady = true;
} }
this.vReady = true;
};
SoundManager.prototype.sumArray = function(array, low, high) { sumArray(array, low, high) {
let total = 0; let total = 0;
for(let i = low; i <= high; i++) { for(let i = low; i <= high; i++) {
total += array[i]; total += array[i];
}
return total/(high-low+1);
} }
return total/(high-low+1);
};
SoundManager.prototype.getVisualiserData = function() { getVisualiserData() {
if(!this.vReady) { if(!this.vReady) {
return null; return null;
}
for(let a = 0; a < this.analyserArrays.length; a++) {
let data = this.analyserArrays[a];
let result = this.logArrays[a];
this.analysers[a].getByteFrequencyData(data);
for(let i = 0; i < this.linBins; i++) {
let scaled = Math.round(i * this.maxBinLin / this.linBins);
result[i] = data[scaled];
}
result[this.linBins] = data[this.binCutoffs[0]];
for(let i = this.linBins+1; i < this.vBars; i++) {
let cutoff = i - this.linBins;
result[i] = this.sumArray(data, this.binCutoffs[cutoff-1],
this.binCutoffs[cutoff]);
}
}
return this.logArrays;
} }
for(let a = 0; a < this.analyserArrays.length; a++) {
let data = this.analyserArrays[a]; setMute(mute) {
let result = this.logArrays[a]; if(!this.mute && mute) { // muting
this.analysers[a].getByteFrequencyData(data); this.lastVol = this.gainNode.gain.value;
for(let i = 0; i < this.linBins; i++) {
let scaled = Math.round(i * this.maxBinLin / this.linBins);
result[i] = data[scaled];
} }
result[this.linBins] = data[this.binCutoffs[0]]; if(mute) {
for(let i = this.linBins+1; i < this.vBars; i++) { this.gainNode.gain.value = 0;
let cutoff = i - this.linBins; } else {
result[i] = this.sumArray(data, this.binCutoffs[cutoff-1], this.gainNode.gain.value = this.lastVol;
this.binCutoffs[cutoff]);
} }
this.core.userInterface.updateVolume(this.gainNode.gain.value);
this.mute = mute;
return mute;
} }
return this.logArrays;
};
SoundManager.prototype.setMute = function(mute) { toggleMute() {
if(!this.mute && mute) { // muting return this.setMute(!this.mute);
this.lastVol = this.gainNode.gain.value;
} }
if(mute) {
this.gainNode.gain.value = 0; decreaseVolume() {
} else { this.setMute(false);
this.gainNode.gain.value = this.lastVol; let val = Math.max(this.gainNode.gain.value - 0.1, 0);
this.setVolume(val);
} }
this.core.userInterface.updateVolume(this.gainNode.gain.value);
this.mute = mute; increaseVolume() {
return mute; this.setMute(false);
}; let val = Math.min(this.gainNode.gain.value + 0.1, 1);
this.setVolume(val);
SoundManager.prototype.toggleMute = function() { }
return this.setMute(!this.mute);
}; setVolume(vol) {
this.gainNode.gain.value = vol;
SoundManager.prototype.decreaseVolume = function() { this.lastVol = vol;
this.setMute(false); this.core.userInterface.updateVolume(vol);
let val = Math.max(this.gainNode.gain.value - 0.1, 0);
this.setVolume(val);
};
SoundManager.prototype.increaseVolume = function() {
this.setMute(false);
let val = Math.min(this.gainNode.gain.value + 0.1, 1);
this.setVolume(val);
};
SoundManager.prototype.setVolume = function(vol) {
this.gainNode.gain.value = vol;
this.lastVol = vol;
this.core.userInterface.updateVolume(vol);
};
SoundManager.prototype.fadeOut = function(callback) {
if(!this.mute) {
// Firefox hackery
this.gainNode.gain.setValueAtTime(this.lastVol, this.context.currentTime);
this.gainNode.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + 2);
} }
setTimeout(callback, 2000);
}; fadeOut(callback) {
if(!this.mute) {
// Firefox hackery
this.gainNode.gain.setValueAtTime(this.lastVol, this.context.currentTime);
this.gainNode.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + 2);
}
setTimeout(callback, 2000);
}
}
let miniOggRaw = let miniOggRaw =
"T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" + "T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" +

Loading…
Cancel
Save