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",
"devDependencies": {
"babel-preset-es2015": "^6.6.0",
"babel-preset-es2015": "^6.9.0",
"del": "^2.2.0",
"gulp": "^3.9.1",
"gulp-autoprefixer": "^3.1.0",
@ -33,6 +33,6 @@
"gulp-plumber": "^1.1.0",
"gulp-sourcemaps": "^1.6.0",
"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
getting the current time with reasonable accuracy */
function HuesCanvas(root, audioContext, core) {
this.audio = audioContext;
core.addEventListener("newimage", this.setImage.bind(this));
core.addEventListener("newcolour", this.setColour.bind(this));
core.addEventListener("beat", this.beat.bind(this));
core.addEventListener("invert", this.setInvert.bind(this));
core.addEventListener("settingsupdated", this.settingsUpdated.bind(this));
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();
}
class HuesCanvas {
constructor(root, audioContext, core) {
this.audio = audioContext;
core.addEventListener("newimage", this.setImage.bind(this));
core.addEventListener("newcolour", this.setColour.bind(this));
core.addEventListener("beat", this.beat.bind(this));
core.addEventListener("invert", this.setInvert.bind(this));
core.addEventListener("settingsupdated", this.settingsUpdated.bind(this));
core.addEventListener("frame", this.animationLoop.bind(this));
this.core = core;
HuesCanvas.prototype.setInvert = function(invert) {
this.invert = invert;
this.needsRedraw = true;
};
HuesCanvas.prototype.settingsUpdated = function() {
this.setSmartAlign(localStorage["smartAlign"]);
this.setBlurAmount(localStorage["blurAmount"]);
this.setBlurDecay(localStorage["blurDecay"]);
this.setBlurQuality(localStorage["blurQuality"]);
this.trippyOn = localStorage["trippyMode"] == "on";
};
HuesCanvas.prototype.resize = function() {
// 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;
};
HuesCanvas.prototype.redraw = function() {
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.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();
}
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.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)) {
let bitmap = this.image.animated ?
this.image.bitmaps[this.animFrame] : this.image.bitmap;
let drawHeight = bitmap.height * (height / bitmap.height);
let drawWidth = (bitmap.width / bitmap.height) * drawHeight;
if(this.smartAlign) {
switch(this.image.align) {
case "left":
offset = 0;
break;
case "right":
offset = width - drawWidth;
break;
default:
offset = width/2 - drawWidth/2;
break;
if(this.image && (this.image.bitmap || this.image.bitmaps)) {
let bitmap = this.image.animated ?
this.image.bitmaps[this.animFrame] : this.image.bitmap;
let drawHeight = bitmap.height * (height / bitmap.height);
let drawWidth = (bitmap.width / bitmap.height) * drawHeight;
if(this.smartAlign) {
switch(this.image.align) {
case "left":
offset = 0;
break;
case "right":
offset = width - drawWidth;
break;
default:
offset = width/2 - drawWidth/2;
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 {
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 = this.blurAlpha;
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();
}
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.blurIterations < 0) {
this.context.globalAlpha = 1;
@ -188,13 +225,11 @@ HuesCanvas.prototype.redraw = function() {
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
// So the base colour is inverted for y, normal for x
// Thus if the y start is more recent, we invert
@ -222,289 +257,265 @@ HuesCanvas.prototype.redraw = function() {
this.offContext.closePath();
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() {
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);
/* Second fastest method from
http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript
It stil does millions of ops per second, and isn't ugly like the integer if/else */
intToHex(num) {
return '#' + ("00000"+num.toString(16)).slice(-6);
}
};
/* Second fastest method from
http://stackoverflow.com/questions/10073699/pad-a-number-with-leading-zeros-in-javascript
It stil does millions of ops per second, and isn't ugly like the integer if/else */
HuesCanvas.prototype.intToHex = function(num) {
return '#' + ("00000"+num.toString(16)).slice(-6);
};
HuesCanvas.prototype.animationLoop = function() {
if (this.colourFade) {
let delta = this.audio.currentTime - this.colourFadeStart;
let fadeVal = delta / this.colourFadeLength;
if (fadeVal >= 1) {
this.stopFade();
this.colour = this.newColour;
} else {
this.mixColours(fadeVal);
animationLoop() {
if (this.colourFade) {
let delta = this.audio.currentTime - this.colourFadeStart;
let fadeVal = delta / this.colourFadeLength;
if (fadeVal >= 1) {
this.stopFade();
this.colour = this.newColour;
} else {
this.mixColours(fadeVal);
}
this.needsRedraw = true;
}
this.needsRedraw = true;
}
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) {
let a = this.animFrame;
this.syncAnim();
if(this.animFrame != a) {
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) {
let a = this.animFrame;
this.syncAnim();
if(this.animFrame != a) {
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++;
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;
// 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;
}
if(this.blurStart) {
// flash offsets blur gen by a frame
let delta = this.audio.currentTime - this.blurStart + (1/30);
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;
}
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
let delta = this.audio.currentTime - this.blurStart + (1/30);
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];
}
setImage(image) {
if(this.image == image) {
return;
}
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) {
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();
beat() {
this.lastBeat = this.audio.currentTime;
}
};
HuesCanvas.prototype.setImage = function(image) {
if(this.image == image) {
return;
syncAnim() {
let song = this.core.currentSong;
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;
// Null images don't need anything interesting done to them
if(!image || (!image.bitmap && !image.bitmaps)) {
return;
setColour(colour, isFade) {
if(colour.c == this.colour) {
return;
}
if(isFade) {
this.newColour = colour.c;
} else {
this.stopFade();
this.colour = colour.c;
}
this.needsRedraw = true;
}
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();
doBlackout(whiteout) {
if (typeof(whiteout)==='undefined') whiteout = false;
if(whiteout) {
this.blackoutColour = "#FFF";
} 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() {
this.lastBeat = this.audio.currentTime;
};
// for song changes
clearBlackout() {
this.blackout = false;
this.blackoutTimeout = 0;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.show();
}
}
HuesCanvas.prototype.syncAnim = function() {
let song = this.core.currentSong;
if(!song) { // fallback to default
return;
doShortBlackout(beatTime) {
this.doBlackout();
this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7;
// 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
if(this.lastBeat && this.core.getBeatLength()) {
let interp = (this.audio.currentTime - this.lastBeat) / this.core.getBeatLength();
index += Math.min(interp, 1);
doColourFade(length) {
this.colourFade = true;
this.colourFadeLength = length;
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;
this.animFrame = Math.floor(aLen * (beatLoc / this.image.beatsPerAnim));
if(this.image.syncOffset) {
this.animFrame += this.image.syncOffset;
stopFade() {
this.colourFade = false;
this.colourFadeStart = 0;
this.colourFadeLength = 0;
}
// Because negative mods are different in JS
this.animFrame = ((this.animFrame % aLen) + aLen) % aLen;
};
HuesCanvas.prototype.setColour = function(colour, isFade) {
if(colour.c == this.colour) {
return;
mixColours(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;
}
if(isFade) {
this.newColour = colour.c;
} else {
this.stopFade();
this.colour = colour.c;
doXBlur() {
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;
}
this.needsRedraw = true;
};
HuesCanvas.prototype.doBlackout = function(whiteout) {
if (typeof(whiteout)==='undefined') whiteout = false;
if(whiteout) {
this.blackoutColour = "#FFF";
} else {
this.blackoutColour = "#000";
doYBlur() {
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;
}
this.blackoutTimeout = 0; // indefinite
// Don't restart the blackout animation if we're already blacked out
if(!this.blackout) {
this.blackoutStart = this.audio.currentTime;
doTrippyX() {
let saveTrippy = this.trippyOn;
// force trippy
this.trippyOn = true;
this.doXBlur();
this.trippyOn = saveTrippy;
}
this.blackout = true;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.hide();
doTrippyY() {
let saveTrippy = this.trippyOn;
// force trippy
this.trippyOn = true;
this.doYBlur();
this.trippyOn = saveTrippy;
}
};
// for song changes
HuesCanvas.prototype.clearBlackout = function() {
this.blackout = false;
this.blackoutTimeout = 0;
this.needsRedraw = true;
if(localStorage["blackoutUI"] == "on") {
this.core.userInterface.show();
setBlurDecay(decay) {
this.blurDecay = {"slow" : 7.8, "medium" : 14.1, "fast" : 20.8, "faster!" : 28.7}[decay];
}
};
HuesCanvas.prototype.doShortBlackout = function(beatTime) {
this.doBlackout();
this.blackoutTimeout = this.audio.currentTime + beatTime / 1.7;
// looks better if we go right to black
this.blackoutStart = 0;
};
HuesCanvas.prototype.doColourFade = function(length) {
this.colourFade = true;
this.colourFadeLength = length;
this.colourFadeStart = this.audio.currentTime;
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";
};
setBlurQuality(quality) {
this.blurIterations = {"low" : -1, "medium" : 11, "high" : 19, "extreme" : 35}[quality];
this.blurDelta = 1 / (this.blurIterations/2);
this.blurAlpha = 1 / (this.blurIterations/2);
}
setBlurAmount(amount) {
this.blurAmount = {"low" : 48, "medium" : 96, "high" : 384}[amount];
}
setSmartAlign(align) {
this.smartAlign = align == "on";
}
}
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.
*/
let beatGlossary = [
const beatGlossary = [
"x Vertical blur (snare)",
"o Horizontal blur (bass)",
"- No blur",
@ -44,7 +44,7 @@ let beatGlossary = [
"I Invert & change image"
];
let shortcuts = [
const shortcuts = [
"↑↓ Change song",
"←→ Change image",
"[N] Random song",

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

File diff suppressed because it is too large Load Diff

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

File diff suppressed because it is too large Load Diff

@ -22,513 +22,514 @@
(function(window, document) {
"use strict";
let debugConsole = false;
const debugConsole = false;
function debug() {
if(debugConsole) {
console.log.apply(window.console, arguments);
}
}
function Respack() {
this.songs = [];
this.songQueue = [];
this.images = [];
this.imageQueue = [];
const audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i");
const imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i");
const animRegex = new RegExp("(.*?)_\\d+$");
this.name = "<no name>";
this.author = "<unknown>";
this.description = "<no description>";
this.link = null;
class Respack {
constructor() {
this.songs = [];
this.songQueue = [];
this.images = [];
this.imageQueue = [];
this.size = -1;
this.downloaded = -1;
this.enabled = true;
this.name = "<no name>";
this.author = "<unknown>";
this.description = "<no description>";
this.link = null;
this._xmlQueue = [];
this.size = -1;
this.downloaded = -1;
this.enabled = true;
this.totalFiles = -1;
// For zip parsing progress events
this.progressCallback = null;
this.filesToLoad = 0;
this.filesLoaded = 0;
this.loadedFromURL = false;
}
this._xmlQueue = [];
Respack.prototype.audioExtensions = new RegExp("\\.(mp3|ogg|wav)$", "i");
Respack.prototype.imageExtensions = new RegExp("\\.(png|gif|jpg|jpeg)$", "i");
Respack.prototype.animRegex = new RegExp("(.*?)_\\d+$");
this.totalFiles = -1;
Respack.prototype.updateProgress = function(override) {
if(this.progressCallback) {
let percent = this.filesLoaded / this.filesToLoad;
if(this.loadedFromURL) {
percent = (percent / 2) + 0.5;
}
this.progressCallback(typeof override === "number" ? override : percent, this);
// For zip parsing progress events
this.progressCallback = null;
this.filesToLoad = 0;
this.filesLoaded = 0;
this.loadedFromURL = false;
}
};
Respack.prototype.loadFromURL = function(url, progress) {
this.loadedFromURL = true;
if(progress) {
this.progressCallback = progress;
updateProgress(override) {
if(this.progressCallback) {
let percent = this.filesLoaded / this.filesToLoad;
if(this.loadedFromURL) {
percent = (percent / 2) + 0.5;
}
this.progressCallback(typeof override === "number" ? override : percent, this);
}
}
return this.getBlob(url)
.then(response => {
return this.loadFromBlob(response);
});
};
loadFromURL(url, progress) {
this.loadedFromURL = true;
if(progress) {
this.progressCallback = progress;
}
Respack.prototype.getBlob = function(url, progress) {
if(progress) {
this.progressCallback = progress;
return this.getBlob(url)
.then(response => {
return this.loadFromBlob(response);
});
}
return new Promise ((resolve, reject) => {
let req = new XMLHttpRequest();
req.open('GET', url, true);
req.responseType = 'blob';
req.onload = () => {
if(req.status == 200) {
resolve(req.response);
} else {
getBlob(url, progress) {
if(progress) {
this.progressCallback = progress;
}
return new Promise ((resolve, reject) => {
let req = new XMLHttpRequest();
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));
}
};
req.onerror = function() {
reject(Error(req.status + ": Could not fetch respack at " + url));
};
req.onprogress = event => {
if (event.lengthComputable) {
this.size = event.total;
this.downloaded = event.loaded;
let percent = event.loaded / event.total;
if(this.progressCallback) {
this.progressCallback(percent / 2, this); // because of processing too
};
req.onprogress = event => {
if (event.lengthComputable) {
this.size = event.total;
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) {
if(progress) {
this.progressCallback = progress;
loadFromBlob(blob, progress) {
if(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) {
let entries = zip.entries;
parseZip(zip) {
let entries = zip.entries;
this.totalFiles = 0;
// Progress events
this.filesToLoad = 0;
this.filesLoaded = 0;
this.totalFiles = 0;
// Progress events
this.filesToLoad = 0;
this.filesLoaded = 0;
// Get everything started
for(let i = 0; i < entries.length; i++) {
if(!entries[i].directory && entries[i].name) {
this.totalFiles++;
this.parseFile(entries[i]);
// Get everything started
for(let i = 0; i < entries.length; i++) {
if(!entries[i].directory && entries[i].name) {
this.totalFiles++;
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()
.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.");
});
};
parseFile(file) {
let name = file.name;
if (name.match(audioExtensions)) {
this.songQueue.push(this.parseSong(file));
this.filesToLoad++;
} else if (name.match(imageExtensions)) {
this.imageQueue.push(this.parseImage(file));
this.filesToLoad++;
} else if(name.toLowerCase().endsWith(".xml")){
this._xmlQueue.push(this.loadXML(file));
}
}
Respack.prototype.parseFile = function(file) {
let name = file.name;
if (name.match(this.audioExtensions)) {
this.songQueue.push(this.parseSong(file));
this.filesToLoad++;
} else if (name.match(this.imageExtensions)) {
this.imageQueue.push(this.parseImage(file));
this.filesToLoad++;
parseSong(file) {
let name = file.name.replace(audioExtensions, "");
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 = "";
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) {
let name = file.name.replace(this.audioExtensions, "");
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();
loadImage(imgFile, imageObj) {
let extension = imgFile.name.split('.').pop().toLowerCase();
let mime = "";
switch(extension) {
case "mp3":
mime = "audio/mpeg3";
case "png":
mime = "image/png";
break;
case "ogg":
mime = "audio/ogg";
case "gif":
mime = "image/gif";
break;
case "wav":
mime = "audio/wav";
case "jpg":
case "jpeg":
mime = "image/jpeg";
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;
imgFile.getData64URI(mime, resolve);
}).then(bitmap => {
this.filesLoaded++;
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);
};
Respack.prototype.loadImage = function(imgFile, imageObj) {
let extension = imgFile.name.split('.').pop().toLowerCase();
let mime = "";
switch(extension) {
case "png":
mime = "image/png";
break;
case "gif":
mime = "image/gif";
break;
case "jpg":
case "jpeg":
mime = "image/jpeg";
break;
default:
mime = "application/octet-stream";
parseImageQueue() {
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());
}
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() {
for(let i = 0; i < this._xmlQueue.length; i++) {
this._xmlQueue[i] = this._xmlQueue[i].then(dom => {
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;
}
loadXML(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);
});
});
}
return Promise.all(this._xmlQueue);
};
// Save some chars
Element.prototype.getTag = function(tag, def) {
let t = this.getElementsByTagName(tag)[0];
return t ? t.textContent : (def ? def : null);
};
parseXML() {
for(let i = 0; i < this._xmlQueue.length; i++) {
this._xmlQueue[i] = this._xmlQueue[i].then(dom => {
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) {
debug(" - Parsing songFile");
let newSongs = [];
let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) {
let song = this.getSong(el.attributes[0].value);
if(song) {
song.title = el.getTag("title");
if(!song.title) {
song.title = "<no name>";
debug(" WARNING!", song.name, "has no title!");
}
parseSongFile(dom) {
debug(" - Parsing songFile");
let newSongs = [];
let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) {
let song = this.getSong(el.attributes[0].value);
if(song) {
song.title = el.getTag("title");
if(!song.title) {
song.title = "<no name>";
debug(" WARNING!", song.name, "has no title!");
}
song.rhythm = el.getTag("rhythm");
if(!song.rhythm) {
song.rhythm = "..no..rhythm..";
debug(" WARNING!!", song.name, "has no rhythm!!");
}
song.rhythm = el.getTag("rhythm");
if(!song.rhythm) {
song.rhythm = "..no..rhythm..";
debug(" WARNING!!", song.name, "has no rhythm!!");
}
song.buildupName = el.getTag("buildup");
if(song.buildupName) {
debug(" Finding a buildup '" + song.buildupName + "' for ", song.name);
let build = this.getSong(song.buildupName);
if(build) {
song.buildup = build.sound;
song.buildupPlayed = false;
// get rid of the junk
this.songs.splice(this.songs.indexOf(build), 1);
} else {
debug(" WARNING!", "Didn't find a buildup '" + song.buildupName + "'!");
song.buildupName = el.getTag("buildup");
if(song.buildupName) {
debug(" Finding a buildup '" + song.buildupName + "' for ", song.name);
let build = this.getSong(song.buildupName);
if(build) {
song.buildup = build.sound;
song.buildupPlayed = false;
// get rid of the junk
this.songs.splice(this.songs.indexOf(build), 1);
} else {
debug(" WARNING!", "Didn't find a buildup '" + song.buildupName + "'!");
}
}
}
song.buildupRhythm = el.getTag("buildupRhythm");
song.independentBuild = el.getTag("independentBuild");
song.source = el.getTag("source");
song.charsPerBeat = parseFloat(el.getTag("charsPerBeat"));
song.buildupRhythm = el.getTag("buildupRhythm");
song.independentBuild = el.getTag("independentBuild");
song.source = el.getTag("source");
song.charsPerBeat = parseFloat(el.getTag("charsPerBeat"));
// Because PackShit breaks everything
if(this.name == "PackShit") {
song.forceTrim = true;
// Because PackShit breaks everything
if(this.name == "PackShit") {
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++) {
if(newSongs.indexOf(this.songs[i]) == -1) {
debug(" WARNING!", "We have a file for", this.songs[i].name, "but no information for it");
for(let i = 0; i < this.songs.length; i++) {
if(newSongs.indexOf(this.songs[i]) == -1) {
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) {
debug(" - Parsing infoFile");
parseInfoFile(dom) {
debug(" - Parsing infoFile");
let info = dom.documentElement;
let info = dom.documentElement;
// self reference strings to avoid changing strings twice in future
this.name = info.getTag("name", this.name);
this.author = info.getTag("author", this.author);
this.description = info.getTag("description", this.description);
this.link = info.getTag("link", this.link);
};
// self reference strings to avoid changing strings twice in future
this.name = info.getTag("name", this.name);
this.author = info.getTag("author", this.author);
this.description = info.getTag("description", this.description);
this.link = info.getTag("link", this.link);
}
Respack.prototype.parseImageFile = function(dom) {
debug(" - Parsing imagefile");
let newImages = [];
let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) {
let image = this.getImage(el.attributes[0].value);
if(image) {
image.fullname = el.getTag("fullname");
if(!image.fullname) {
debug(" WARNING!", image.name, "has no full name!");
}
image.source = el.getTag("source");
// self reference defaults to avoid changing strings twice in future
image.align = el.getTag("align", image.align);
image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim"));
image.syncOffset = parseFloat(el.getTag("syncOffset"));
let frameDur = el.getTag("frameDuration");
if(frameDur) {
image.frameDurations = [];
let strSplit = frameDur.split(",");
for(let j = 0; j < strSplit.length; j++) {
image.frameDurations.push(parseInt(strSplit[j]));
parseImageFile(dom) {
debug(" - Parsing imagefile");
let newImages = [];
let el = dom.documentElement.firstElementChild;
for(; el; el = el.nextElementSibling) {
let image = this.getImage(el.attributes[0].value);
if(image) {
image.fullname = el.getTag("fullname");
if(!image.fullname) {
debug(" WARNING!", image.name, "has no full name!");
}
image.source = el.getTag("source");
// self reference defaults to avoid changing strings twice in future
image.align = el.getTag("align", image.align);
image.beatsPerAnim = parseFloat(el.getTag("beatsPerAnim"));
image.syncOffset = parseFloat(el.getTag("syncOffset"));
let frameDur = el.getTag("frameDuration");
if(frameDur) {
image.frameDurations = [];
let strSplit = frameDur.split(",");
for(let j = 0; j < strSplit.length; 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) {
image.frameDurations.push(image.frameDurations[image.frameDurations.length - 1]);
debug(" [I] " + image.name, ":", image.fullname, "added to images");
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);
}
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) {
return this.getSong(name) !== null;
};
containsSong(name) {
return this.getSong(name) !== null;
}
Respack.prototype.containsImage = function(name) {
return this.getImage(name) !== null;
};
containsImage(name) {
return this.getImage(name) !== null;
}
Respack.prototype.getSong = function(name) {
for(let i = 0; i < this.songs.length; i++) {
if (name == this.songs[i].name) {
return this.songs[i];
getSong(name) {
for(let i = 0; i < this.songs.length; i++) {
if (name == this.songs[i].name) {
return this.songs[i];
}
}
return null;
}
return null;
};
Respack.prototype.getImage = function(name) {
for(let i = 0; i < this.images.length; i++) {
if (name == this.images[i].name) {
return this.images[i];
getImage(name) {
for(let i = 0; i < this.images.length; i++) {
if (name == this.images[i].name) {
return this.images[i];
}
}
return null;
}
return null;
};
}
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);

@ -22,531 +22,533 @@
(function(window, document) {
"use strict";
function SoundManager(core) {
this.core = core;
this.playing = false;
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;
}
class SoundManager {
constructor(core) {
this.core = core;
this.playing = false;
this.playbackRate = 1;
this.song = null;
SoundManager.prototype.init = function() {
if(!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
// Check Web Audio API Support
try {
// 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) {
reject(Error("Web Audio API not supported in this browser."));
return;
}
resolve();
}).then(() => {
// check for .ogg support - if not, we'll have to load the ogg decoder
return new Promise((resolve, reject) => {
this.context.decodeAudioData(miniOgg, success => {
this.oggSupport = true;
resolve();
}, error => {
this.oggSupport = false;
resolve();
});
});
}).then(() => {
return new Promise((resolve, reject) => {
// See if our audio decoder is working
let audioWorker;
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;
}
init() {
if(!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
// Check Web Audio API Support
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) {
console.log(e);
reject(Error("Audio Worker cannot be started - correct path set in defaults?"));
reject(Error("Web Audio API not supported in this browser."));
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});
resolve();
}).then(() => {
// check for .ogg support - if not, we'll have to load the ogg decoder
return new Promise((resolve, reject) => {
this.context.decodeAudioData(miniOgg, success => {
this.oggSupport = true;
resolve();
}, error => {
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() {
if(this.lockedPromise) {
unlock() {
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;
}
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;
};
SoundManager.prototype.playSong = function(song, playBuild, forcePlay) {
let p = Promise.resolve();
// Editor forces play on audio updates
if(this.song == song && !forcePlay) {
playSong(song, playBuild, forcePlay) {
let p = Promise.resolve();
// Editor forces play on audio updates
if(this.song == song && !forcePlay) {
return p;
}
this.stop();
this.song = song;
if(!song || (!song.sound)) { // null song
return p;
}
// if there's a fadeout happening from AutoSong, kill it
this.gainNode.gain.cancelScheduledValues(0);
// Reset original volume
this.setVolume(this.lastVol);
if(this.mute) {
this.setMute(true);
}
p = p.then(() => {
return this.loadSong(song);
}).then(buffers => {
// 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");
}
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;
}
this.stop();
this.song = song;
if(!song || (!song.sound)) { // null song
return p;
stop(dontDeleteBuffers) {
if (this.playing) {
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
this.gainNode.gain.cancelScheduledValues(0);
// Reset original volume
this.setVolume(this.lastVol);
if(this.mute) {
this.setMute(true);
setRate(rate) {
// Double speed is more than enough. Famous last words?
rate = Math.max(Math.min(rate, 2), 0.25);
let time = this.clampedTime();
this.playbackRate = rate;
this.seek(time);
}
p = p.then(() => {
return this.loadSong(song);
}).then(buffers => {
// 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");
seek(time, noPlayingUpdate) {
if(!this.song) {
return;
}
//console.log("Seeking to " + time);
// Clamp the blighter
time = Math.min(Math.max(time, -this.buildLength), this.loopLength);
this.buildup = buffers.buildup;
this.buildLength = this.buildup ? this.buildup.duration : 0;
this.loop = buffers.loop;
this.loopLength = this.loop.duration;
this.stop(true);
// 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);
if(!this.loop) {
return;
}
return this.context.resume();
}).then(() => {
this.playing = true;
});
return p;
};
this.loopSource = this.context.createBufferSource();
this.loopSource.buffer = this.loop;
this.loopSource.playbackRate.value = this.playbackRate;
this.loopSource.loop = true;
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) {
if (this.playing) {
if(this.buildSource) {
this.buildSource.stop(0);
this.buildSource.disconnect();
this.buildSource = null;
if(!dontDeleteBuffers)
this.buildup = null;
this.startTime = this.context.currentTime - (time / this.playbackRate);
if(!noPlayingUpdate) {
this.playing = true;
}
// 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;
this.initVisualiser();
this.core.recalcBeatIndex();
}
};
SoundManager.prototype.setRate = function(rate) {
// Double speed is more than enough. Famous last words?
rate = Math.max(Math.min(rate, 2), 0.25);
// In seconds, relative to the loop start
currentTime() {
if(!this.playing) {
return 0;
}
return (this.context.currentTime - this.startTime) * this.playbackRate;
}
let time = this.clampedTime();
this.playbackRate = rate;
this.seek(time);
};
clampedTime() {
let time = this.currentTime();
SoundManager.prototype.seek = function(time, noPlayingUpdate) {
if(!this.song) {
return;
if(time > 0) {
time %= this.loopLength;
}
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) {
return;
}
let buffers = {loop: null, buildup: null};
this.loopSource = this.context.createBufferSource();
this.loopSource.buffer = this.loop;
this.loopSource.playbackRate.value = this.playbackRate;
this.loopSource.loop = true;
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);
let promises = [this.loadBuffer(song, "sound").then(buffer => {
buffers.loop = buffer;
})];
if(song.buildup) {
promises.push(this.loadBuffer(song, "buildup").then(buffer => {
buffers.buildup = buffer;
}));
} else {
this.buildLength = 0;
}
song._loadPromise = Promise.all(promises)
.then(() => {
song._loadPromise = null;
return buffers;
});
return song._loadPromise;
}
this.startTime = this.context.currentTime - (time / this.playbackRate);
if(!noPlayingUpdate) {
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;
};
loadBuffer(song, soundName) {
let buffer = song[soundName];
SoundManager.prototype.clampedTime = function() {
let time = this.currentTime();
// 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();
if(time > 0) {
time %= this.loopLength;
}
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");
}
audioWorker.addEventListener('error', () => {
reject(Error("Audio Worker failed to convert track"));
}, false);
let buffers = {loop: null, buildup: null};
audioWorker.addEventListener('message', e => {
let decoded = e.data;
audioWorker.terminate();
let promises = [this.loadBuffer(song, "sound").then(buffer => {
buffers.loop = buffer;
})];
if(song.buildup) {
promises.push(this.loadBuffer(song, "buildup").then(buffer => {
buffers.buildup = buffer;
}));
} else {
this.buildLength = 0;
}
song._loadPromise = Promise.all(promises)
.then(() => {
song._loadPromise = null;
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"));
// 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]);
});
}).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() {
return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js');
};
SoundManager.prototype.initVisualiser = function(bars) {
// When restarting the visualiser
if(!bars) {
bars = this.vTotalBars;
}
this.vReady = false;
this.vTotalBars = bars;
for(let i = 0; i < this.analysers.length; i++) {
this.analysers[i].disconnect();
// Converts continuous PCM array to Web Audio API friendly format
audioBufFromRaw(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;
}
if(this.splitter) {
this.splitter.disconnect();
this.splitter = null;
createWorker() {
return new Worker(this.core.settings.defaults.workersPath + 'audio-worker.js');
}
this.analysers = [];
this.analyserArrays = [];
this.logArrays = [];
this.binCutoffs = [];
this.linBins = 0;
this.logBins = 0;
this.maxBinLin = 0;
initVisualiser(bars) {
// When restarting the visualiser
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() {
if(!this.playing || this.vReady) {
return;
this.attachVisualiser();
}
// Get our info from the loop
let channels = this.loopSource.channelCount;
// In case channel counts change, this is changed each time
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;
attachVisualiser() {
if(!this.playing || this.vReady) {
return;
}
// Chosen because they look nice, no maths behind it
analyser.smoothingTimeConstant = 0.6;
analyser.minDecibels = -70;
analyser.maxDecibels = -25;
this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount));
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);
// Get our info from the loop
let channels = this.loopSource.channelCount;
// In case channel counts change, this is changed each time
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;
analyser.minDecibels = -70;
analyser.maxDecibels = -25;
this.analyserArrays.push(new Uint8Array(analyser.frequencyBinCount));
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) {
let total = 0;
for(let i = low; i <= high; i++) {
total += array[i];
sumArray(array, low, high) {
let total = 0;
for(let i = low; i <= high; i++) {
total += array[i];
}
return total/(high-low+1);
}
return total/(high-low+1);
};
SoundManager.prototype.getVisualiserData = function() {
if(!this.vReady) {
return null;
getVisualiserData() {
if(!this.vReady) {
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];
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];
setMute(mute) {
if(!this.mute && mute) { // muting
this.lastVol = this.gainNode.gain.value;
}
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]);
if(mute) {
this.gainNode.gain.value = 0;
} else {
this.gainNode.gain.value = this.lastVol;
}
this.core.userInterface.updateVolume(this.gainNode.gain.value);
this.mute = mute;
return mute;
}
return this.logArrays;
};
SoundManager.prototype.setMute = function(mute) {
if(!this.mute && mute) { // muting
this.lastVol = this.gainNode.gain.value;
toggleMute() {
return this.setMute(!this.mute);
}
if(mute) {
this.gainNode.gain.value = 0;
} else {
this.gainNode.gain.value = this.lastVol;
decreaseVolume() {
this.setMute(false);
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;
return mute;
};
SoundManager.prototype.toggleMute = function() {
return this.setMute(!this.mute);
};
SoundManager.prototype.decreaseVolume = function() {
this.setMute(false);
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);
increaseVolume() {
this.setMute(false);
let val = Math.min(this.gainNode.gain.value + 0.1, 1);
this.setVolume(val);
}
setVolume(vol) {
this.gainNode.gain.value = vol;
this.lastVol = vol;
this.core.userInterface.updateVolume(vol);
}
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 =
"T2dnUwACAAAAAAAAAADFYgAAAAAAAMLKRdwBHgF2b3JiaXMAAAAAAUSsAAAA" +

Loading…
Cancel
Save