From 57737b0c75beb242738dab36a61389b113170af6 Mon Sep 17 00:00:00 2001 From: William Toohey Date: Thu, 5 Nov 2015 21:39:57 +1000 Subject: [PATCH] Add sweet audio visualiser Also some minor CSS tweaks --- css/hues-h.css | 13 +++++ css/hues-m.css | 22 ++++++-- css/hues-r.css | 10 ++++ css/hues-w.css | 4 ++ css/hues-x.css | 11 ++++ css/style.css | 12 +++++ js/HuesCore.js | 62 ++++++++++++++++++++++ js/HuesSettings.js | 10 +++- js/HuesUI.js | 26 +++++++++ js/SoundManager.js | 129 ++++++++++++++++++++++++++++++++++++++++++++- 10 files changed, 292 insertions(+), 7 deletions(-) diff --git a/css/hues-h.css b/css/hues-h.css index 04855f5..5ae9154 100644 --- a/css/hues-h.css +++ b/css/hues-h.css @@ -34,6 +34,11 @@ font-size: 13px; } +.hues-m-beatcenter.hues-h-text.hidden{ + transform: translateY(-80px); + -webkit-transform: translateY(-80px); +} + .hues-h-eyes { background: none; background-image: url('../img/skull-eyes.png'); @@ -77,6 +82,13 @@ z-index: 1; } +@media (min-width: 768px) { + .hues-m-controls.hues-h-controls.hidden { + transform: translateY(64px); + -webkit-transform: translateY(64px); + } +} + .hues-m-songtitle.hues-h-text, .hues-m-imagename.hues-h-text { padding: 4px 0px; margin: 0px 5px; @@ -210,4 +222,5 @@ width: 100%; height: 100%; position: absolute; + z-index: -1; } \ No newline at end of file diff --git a/css/hues-m.css b/css/hues-m.css index 61cd5ba..c5229c6 100644 --- a/css/hues-m.css +++ b/css/hues-m.css @@ -25,8 +25,18 @@ } .hues-m-controls.hidden { - transform: translateY(104px); - -webkit-transform: translateY(104px); + transform: translateY(108px); + -webkit-transform: translateY(108px); +} + +.hues-m-visualisercontainer { + position: absolute; + width: 100%; + height: 64px; + bottom:108px; + left:-8px; + right:0px; + margin:0px auto; } .hues-m-beatbar { @@ -166,7 +176,6 @@ max-width: 992px; height: 104px; margin: 0 auto; - overflow: hidden; left: 8px; right: 8px; color: rgba(255,255,255,0.7); @@ -462,6 +471,10 @@ input[type=range]::-ms-thumb { .hues-m-controls { height: 54px; } + .hues-m-controls.hidden { + transform: translateY(54px); + -webkit-transform: translateY(54px); + } .hues-m-imagename { left: 300px; right: 300px; @@ -499,4 +512,7 @@ input[type=range]::-ms-thumb { margin: 0 auto; left: 8px; } + .hues-m-visualisercontainer { + bottom:58px; + } } diff --git a/css/hues-r.css b/css/hues-r.css index 63ca03f..b31de30 100644 --- a/css/hues-r.css +++ b/css/hues-r.css @@ -112,4 +112,14 @@ position: absolute; right: 35px; bottom: 45px; +} + +.hues-r-visualisercontainer { + transform: scaleY(-1); + -webkit-transform: scaleY(-1); + position: absolute; + width: 100%; + height: 64px; + top: 0; + left: 0; } \ No newline at end of file diff --git a/css/hues-w.css b/css/hues-w.css index afb81bf..e0f3073 100644 --- a/css/hues-w.css +++ b/css/hues-w.css @@ -90,4 +90,8 @@ @-webkit-keyframes fallspin { from {-webkit-transform: rotate(0deg) translate(0px, 0px); opacity: 1;} +} + +.hues-r-visualisercontainer.hues-w-visualisercontainer { + top: 17px; } \ No newline at end of file diff --git a/css/hues-x.css b/css/hues-x.css index 864fdbf..2d4c553 100644 --- a/css/hues-x.css +++ b/css/hues-x.css @@ -20,6 +20,7 @@ .hues-m-beatbar.hues-x-beatbar { background: none; border-style: none; + overflow: visible; } .hues-x-light { @@ -96,4 +97,14 @@ left:50%; margin-left: -1444.5px; overflow: hidden; +} + +.hues-x-visualisercontainer { + transform: scaleY(-1); + -webkit-transform: scaleY(-1); + position: absolute; + width: 100%; + height: 64px; + top: 25px; + left: 0; } \ No newline at end of file diff --git a/css/style.css b/css/style.css index 7e47fcd..a1f2d23 100644 --- a/css/style.css +++ b/css/style.css @@ -85,6 +85,15 @@ h1, h2, h3 { display: none; } +#visualiser { + position:absolute; + z-index: -1; +} + +#visualiser.hidden { + display: none; +} + #preloadHelper { background-color: #FFF; width: 100%; @@ -205,6 +214,9 @@ input.tab-input[type="radio"]:checked + label { .settings-category { font-size: 12pt; + width: 50%; + float: left; + margin-bottom: 10px; } .settings-individual{ diff --git a/js/HuesCore.js b/js/HuesCore.js index c18d81e..302049a 100644 --- a/js/HuesCore.js +++ b/js/HuesCore.js @@ -62,6 +62,11 @@ function HuesCore(defaults) { return; } this.renderer = new HuesCanvas("waifu", this.soundManager.context, this); + + this.visualiser = document.createElement("canvas"); + this.visualiser.id = "visualiser"; + this.visualiser.height = "64"; + this.vCtx = this.visualiser.getContext("2d"); this.uiArray.push(new RetroUI(), new WeedUI(), new ModernUI(), new XmasUI(), new HalloweenUI()); this.settings.connectCore(this); @@ -104,12 +109,55 @@ function HuesCore(defaults) { this.animationLoop(); } +HuesCore.prototype.resizeVisualiser = function() { + this.soundManager.initVisualiser(this.visualiser.width/2); +} + +HuesCore.prototype.updateVisualiser = function() { + if(localStorage["visualiser"] != "on") { + return; + } + + var logArrays = this.soundManager.getVisualiserData(); + if(!logArrays) { + return; + } + + this.vCtx.clearRect(0, 0, this.vCtx.canvas.width, this.vCtx.canvas.height); + + var gradient=this.vCtx.createLinearGradient(0,64,0,0); + gradient.addColorStop(1,"rgba(255,255,255,0.6)"); + gradient.addColorStop(0,"rgba(20,20,20,0.6)"); + this.vCtx.fillStyle = gradient; + + var barWidth = 2; + var barHeight; + var x = 0; + for(var a = 0; a < logArrays.length; a++) { + var vals = logArrays[a]; + for(var i = 0; i < vals.length; i++) { + var index = 0; + if(logArrays.length == 2 && a == 0) { + index = vals.length - i - 1; + } else { + index = i; + } + barHeight = vals[index]/4; + + this.vCtx.fillRect(x,this.vCtx.canvas.height-barHeight,barWidth,barHeight); + + x += barWidth; + } + } +} + HuesCore.prototype.animationLoop = function() { var that = this; if(!this.soundManager.playing) { requestAnimationFrame(function() {that.animationLoop();}); return; } + this.updateVisualiser(); var now = this.soundManager.currentTime(); if(now < 0) { this.userInterface.updateTime(0); @@ -271,6 +319,9 @@ HuesCore.prototype.songDataUpdated = function() { HuesCore.prototype.resetAudio = function() { this.beatIndex = 0; this.songDataUpdated(); + if(localStorage["visualiser"] == "on") { + this.soundManager.initVisualiser(this.visualiser.width/2); + } }; HuesCore.prototype.randomImage = function() { @@ -565,6 +616,17 @@ HuesCore.prototype.settingsUpdated = function() { } break; } + switch (localStorage["visualiser"]) { + case "off": + document.getElementById("visualiser").className = "hidden"; + break; + case "on": + document.getElementById("visualiser").className = ""; + if(!this.soundManager.vReady) { + this.soundManager.initVisualiser(this.visualiser.width/2); + } + break; + } /*if (this.autoSong == "off" && !(this.settings.autosong == "off")) { console.log("Resetting loopCount since AutoSong was enabled"); this.loopCount = 0; diff --git a/js/HuesSettings.js b/js/HuesSettings.js index f059ca9..f888247 100644 --- a/js/HuesSettings.js +++ b/js/HuesSettings.js @@ -46,7 +46,8 @@ HuesSettings.prototype.defaultSettings = { colourSet: "normal", blackoutUI: "off", playBuildups: "on", - volume : 0.7 + volume: 0.7, + visualiser: "on" }; // Don't get saved to localStorage @@ -73,7 +74,8 @@ HuesSettings.prototype.settingsCategories = { "UI Settings" : [ "currentUI", "colourSet", - "blackoutUI" + "blackoutUI", + "visualiser" ], "Audio Settings" : [ "playBuildups" @@ -97,6 +99,10 @@ HuesSettings.prototype.settingsOptions = { name : "Blur Quality", options : ["low", "medium", "high", "extreme"] }, + visualiser : { + name : "Spectrum analyser", + options : ["on", "off"] + }, currentUI : { name : "User Interface", options : ["retro", "v4.20", "modern", "xmas", "hlwn"] diff --git a/js/HuesUI.js b/js/HuesUI.js index ff71649..1c66b2a 100644 --- a/js/HuesUI.js +++ b/js/HuesUI.js @@ -58,6 +58,8 @@ function HuesUI(parent) { // Put this near the links to song/image lists/ Bottom right alignment this.listContainer = null; + // Must be dynamic width, 64 pixels high. Will be filled with visualiser + this.visualiserContainer = null; this.hidden = false; @@ -139,6 +141,7 @@ HuesUI.prototype.initUI = function() { }; this.listContainer = document.createElement("div"); + this.visualiserContainer = document.createElement("div"); this.resizeHandler = function() { that.resize(); @@ -149,6 +152,7 @@ HuesUI.prototype.connectCore = function(core) { this.core = core; this.root.style.display = "block"; this.listContainer.appendChild(core.resourceManager.listView); + this.visualiserContainer.appendChild(this.core.visualiser); window.addEventListener('resize', this.resizeHandler); this.resizeHandler(); @@ -160,6 +164,9 @@ HuesUI.prototype.disconnect = function() { while (this.listContainer.firstElementChild) { this.listContainer.removeChild(this.listContainer.firstElementChild); } + while (this.visualiserContainer.firstElementChild) { + this.visualiserContainer.removeChild(this.visualiserContainer.firstElementChild); + } window.removeEventListener('resize', this.resizeHandler); }; @@ -345,6 +352,9 @@ RetroUI.prototype.initUI = function() { this.listContainer.className = "hues-r-listcontainer"; this.root.appendChild(this.listContainer); + + this.visualiserContainer.className = "hues-r-visualisercontainer"; + this.root.appendChild(this.visualiserContainer); }; RetroUI.prototype.toggleHide = function(stylename) { @@ -400,6 +410,11 @@ RetroUI.prototype.beat = function() { this.beatCount.textContent = "B=" + this.intToHex3(this.core.getSafeBeatIndex()); }; +RetroUI.prototype.resize = function() { + this.core.visualiser.width = this.visualiserContainer.offsetWidth; + this.core.resizeVisualiser(); +}; + function WeedUI() { RetroUI.call(this); @@ -435,6 +450,8 @@ WeedUI.prototype.initUI = function() { this.imageModeManual.textContent = "ONE"; this.imageModeAuto.textContent = "MANY"; + + this.visualiserContainer.className += " hues-w-visualisercontainer"; }; WeedUI.prototype.toggleHide = function() { @@ -632,6 +649,9 @@ ModernUI.prototype.initUI = function() { this.leftInfo = leftInfo; controls.appendChild(leftInfo); controls.appendChild(rightInfo); + + this.visualiserContainer.className = "hues-m-visualisercontainer"; + controls.appendChild(this.visualiserContainer); var beatBar = document.createElement("div"); beatBar.className = "hues-m-beatbar"; @@ -723,6 +743,8 @@ ModernUI.prototype.beat = function() { ModernUI.prototype.resize = function() { this.resizeSong(); this.resizeImage(); + this.core.visualiser.width = this.controls.offsetWidth; + this.core.resizeVisualiser(); }; ModernUI.prototype.resizeElement = function(el, parent) { @@ -823,6 +845,10 @@ function XmasUI() { bottomHelper.appendChild(bottom); wires.appendChild(bottomHelper); this.root.appendChild(wires); + + this.visualiserContainer.className = "hues-x-visualisercontainer"; + this.controls.removeChild(this.visualiserContainer); + this.beatBar.appendChild(this.visualiserContainer); } XmasUI.prototype = Object.create(ModernUI.prototype); diff --git a/js/SoundManager.js b/js/SoundManager.js index bfcb208..f42016a 100644 --- a/js/SoundManager.js +++ b/js/SoundManager.js @@ -40,6 +40,18 @@ function SoundManager(core) { this.gainNode = null; this.mute = false; this.lastVol = 1; + + // Visualiser + this.vReady = false; + this.vBars = 0; + this.splitter = null; + this.analysers = []; + this.analyserArrays = []; + this.logArrays = []; + this.binCutoffs = []; + this.linBins = 0; + this.logBins = 0; + this.maxBinLin = 0; // For concatenating our files this.leftToLoad = 0; @@ -124,9 +136,9 @@ SoundManager.prototype.playSong = function(song, playBuild, callback) { that.startTime = that.context.currentTime; } that.context.resume().then(function() { + that.playing = true; if(callback) callback(); - that.playing = true; }); }); } else { @@ -138,9 +150,9 @@ SoundManager.prototype.playSong = function(song, playBuild, callback) { that.bufSource.start(0, that.loopStart); that.startTime = that.context.currentTime; } + that.playing = true; if(callback) callback(); - that.playing = true; } } }); @@ -152,6 +164,7 @@ SoundManager.prototype.stop = function() { this.bufSource.stop(0); this.bufSource.disconnect(); // TODO needed? this.bufSource = null; + this.vReady = false; this.playing = false; this.startTime = 0; this.loopStart = 0; @@ -302,6 +315,118 @@ SoundManager.prototype.concatenateAudioBuffers = function(buffer1, buffer2) { return tmp; }; + +SoundManager.prototype.initVisualiser = function(bars) { + if(!bars) { + return; + } + this.vReady = false; + this.vBars = bars; + for(var 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.linBins = 0; + this.logBins = 0; + this.maxBinLin = 0; + + this.attachVisualiser(); +} + +SoundManager.prototype.attachVisualiser = function() { + if(!this.playing || this.vReady) { + return; + } + + var channels = this.bufSource.channelCount; + // In case channel counts change, this is changed each time + this.splitter = this.context.createChannelSplitter(channels); + this.bufSource.connect(this.splitter); + // Split display up into each channel + this.vBars = Math.floor(this.vBars/channels); + + for(var i = 0; i < channels; i++) { + var 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)); + } + var binCount = this.analysers[0].frequencyBinCount; + var binWidth = this.bufSource.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 + var maxBinLog = Math.floor(22000/binWidth); + var logBins = this.vBars - this.linBins; + console.log("Going to compress", this.maxBinLin, + "bins into", this.linBins, "linear bins"); + console.log("Going to compress", maxBinLog-this.linBins, + "bins into", logBins, "logarithmic bins"); + + var logLow = Math.log2(2000); + var logDiff = Math.log2(22000) - logLow; + for(var i = 0; i < logBins; i++) { + var cutoff = i * (logDiff/logBins) + logLow; + var freqCutoff = Math.pow(2, cutoff); + var binCutoff = Math.floor(freqCutoff / binWidth); + this.binCutoffs.push(binCutoff); + } + this.vReady = true; +} + +SoundManager.prototype.sumArray = function(array, low, high) { + var total = 0; + for(var i = low; i <= high; i++) { + total += array[i]; + } + return total/(high-low+1); +} + +SoundManager.prototype.getVisualiserData = function() { + if(!this.vReady) { + return null; + } + for(var a = 0; a < this.analyserArrays.length; a++) { + var data = this.analyserArrays[a]; + var result = this.logArrays[a]; + this.analysers[a].getByteFrequencyData(data); + + for(var i = 0; i < this.linBins; i++) { + var scaled = Math.round(i * this.maxBinLin / this.linBins); + result[i] = data[scaled]; + } + result[this.linBins] = data[this.binCutoffs[0]]; + for(var i = this.linBins+1; i < this.vBars; i++) { + var cutoff = i - this.linBins; + result[i] = this.sumArray(data, this.binCutoffs[cutoff-1], + this.binCutoffs[cutoff]); + } + } + return this.logArrays; +} + SoundManager.prototype.setMute = function(mute) { if(!this.mute && mute) { // muting this.lastVol = this.gainNode.gain.value;