|
/* ___ ___ ___ ___ ___ ___ |
|
___ /\__\ /\ \ /\ \ /\ \ ___ /\__\ ___ /\ \ |
|
/\ \ /::| | /::\ \ /::\ \ /::\ \ /\ \ /::| | /\ \ \:\ \ |
|
\:\ \ /:|:| | /:/\:\ \ /:/\:\ \ /:/\:\ \ \:\ \ /:|:| | \:\ \ \:\ \ |
|
/::\__\ /:/|:| |__ /:/ \:\__\ /::\~\:\ \ /::\~\:\ \ /::\__\ /:/|:| |__ /::\__\ /::\ \ |
|
__/:/\/__/ /:/ |:| /\__\ /:/__/ \:|__| /:/\:\ \:\__\ /:/\:\ \:\__\ __/:/\/__/ /:/ |:| /\__\ __/:/\/__/ /:/\:\__\ |
|
/\/:/ / \/__|:|/:/ / \:\ \ /:/ / \:\~\:\ \/__/ \/__\:\ \/__/ /\/:/ / \/__|:|/:/ / /\/:/ / /:/ \/__/ |
|
\::/__/ |:/:/ / \:\ /:/ / \:\ \:\__\ \:\__\ \::/__/ |:/:/ / \::/__/ /:/ / |
|
\:\__\ |::/ / \:\/:/ / \:\ \/__/ \/__/ \:\__\ |::/ / \:\__\ \/__/ |
|
\/__/ /:/ / \::/__/ \:\__\ \/__/ /:/ / \/__/ |
|
\/__/ ~~ \/__/ \/__/ |
|
* http://studioindefinit.com |
|
* SONO - An Indefinit Web Audio API Library by Studio Indefinit |
|
*/ |
|
|
|
var SONO = { REV : '1'}; |
|
|
|
(function(){ |
|
var self = this; |
|
var callback, ctx; |
|
|
|
// Force polyfill for Web Audio |
|
// by @jonobr1 / http://jonobr1.com/ |
|
//self.addEventListener('load', function() { |
|
self.AudioContext = self.AudioContext || self.webkitAudioContext; |
|
SONO._ready = true; |
|
try { |
|
SONO.ctx = ctx = new self.AudioContext(); |
|
SONO.has = true; |
|
} catch (e) { |
|
delete SONO.ctx; |
|
SONO.has = false; |
|
} |
|
//}, false); |
|
})(); |
|
|
|
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/ |
|
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating |
|
// requestAnimationFrame polyfill by Erik Möller |
|
// fixes from Paul Irish and Tino Zijdel |
|
// using 'self' instead of 'window' for compatibility with both NodeJS and IE10. |
|
( function () { |
|
|
|
var lastTime = 0; |
|
var vendors = [ 'ms', 'moz', 'webkit', 'o' ]; |
|
|
|
for ( var x = 0; x < vendors.length && !self.requestAnimationFrame; ++ x ) { |
|
|
|
self.requestAnimationFrame = self[ vendors[ x ] + 'RequestAnimationFrame' ]; |
|
self.cancelAnimationFrame = self[ vendors[ x ] + 'CancelAnimationFrame' ] || self[ vendors[ x ] + 'CancelRequestAnimationFrame' ]; |
|
|
|
} |
|
|
|
if ( self.requestAnimationFrame === undefined && self['setTimeout'] !== undefined ) { |
|
|
|
self.requestAnimationFrame = function ( callback ) { |
|
var currTime = Date.now(), timeToCall = Math.max( 0, 16 - ( currTime - lastTime ) ); |
|
var id = self.setTimeout( function() { callback( currTime + timeToCall ); }, timeToCall ); |
|
lastTime = currTime + timeToCall; |
|
return id; |
|
}; |
|
|
|
} |
|
|
|
if( self.cancelAnimationFrame === undefined && self['clearTimeout'] !== undefined ) { |
|
|
|
self.cancelAnimationFrame = function ( id ) { self.clearTimeout( id ) }; |
|
} |
|
|
|
}() ); |
|
|
|
|
|
SONO.BufferLoader = function(audioCtx, urlList, callback) { |
|
this.audioCtx = audioCtx; |
|
this.urlList = urlList; |
|
this.onload = callback ; |
|
this.bufferList = new Array(); |
|
this.loadCount = 0; |
|
} |
|
|
|
SONO.BufferLoader.prototype = { |
|
loadBuffer : function(url, index) { |
|
// Load buffer asynchronously |
|
var request = new XMLHttpRequest(); |
|
request.open("GET", url, true); |
|
request.responseType = "arraybuffer"; |
|
|
|
var loader = this; |
|
|
|
request.onload = function() { |
|
// Asynchronously decode the audio file data in request.response |
|
loader.audioCtx.decodeAudioData( |
|
request.response, |
|
function(buffer) { |
|
if (!buffer) { |
|
alert('error decoding file data: ' + url); |
|
return; |
|
} |
|
loader.bufferList[index] = buffer; |
|
if (++loader.loadCount == loader.urlList.length) |
|
loader.onload(loader.bufferList); //@TODO here |
|
}, |
|
function(error) { |
|
console.error('decodeAudioData error', error); |
|
} |
|
); |
|
} |
|
|
|
request.onerror = function() { |
|
alert('BufferLoader: XHR error'); |
|
} |
|
|
|
request.send(); |
|
}, |
|
load : function() { |
|
for (var i = 0; i < this.urlList.length; ++i) |
|
this.loadBuffer(this.urlList[i], i); |
|
}, |
|
|
|
}; |
|
|
|
SONO.Visualizer = function(bufferList, fftSize, smoothing) { |
|
this.analyser = SONO.ctx.createAnalyser(); |
|
this.smoothing = smoothing || 0.8; |
|
this.fftSize = fftSize || 2048; |
|
this.analyser.connect(SONO.ctx.destination); |
|
this.analyser.minDecibels = -140; |
|
this.analyser.maxDecibels = 0; |
|
this.freqs = new Uint8Array(this.analyser.frequencyBinCount); // data range is from 0 - 256 for 512 bins. no sound is 0; |
|
this.times = new Uint8Array(this.analyser.frequencyBinCount); // data range is from 0-256 for 512 bins. no sound is 128. |
|
this.buffer = bufferList[0]; |
|
this.isPlaying = false; |
|
this.startTime = 0; |
|
this.startOffset = 0; |
|
this.levelsCount = 16; //should be factor of 512 |
|
this.levelBins = Math.floor(this.analyser.frequencyBinCount / this.levelsCount); |
|
this.gotBeat = false; |
|
this.beatTime = 0; |
|
} |
|
|
|
SONO.Visualizer.prototype = { |
|
// Toggle playback |
|
togglePlayback : function() { |
|
if (this.isPlaying) { |
|
// Stop playback |
|
this.source[this.source.stop ? 'stop': 'noteOff'](0); |
|
this.startOffset += SONO.ctx.currentTime - this.startTime; |
|
console.log('paused at', this.startOffset); |
|
// Save the position of the play head. |
|
} else { |
|
this.startTime = SONO.ctx.currentTime; |
|
console.log('started at', this.startOffset); |
|
this.source = SONO.ctx.createBufferSource(); |
|
// Connect graph |
|
this.source.connect(this.analyser); |
|
this.source.buffer = this.buffer; |
|
this.source.loop = true; |
|
// Start playback, but make sure we stay in bound of the buffer. |
|
this.source[this.source.start ? 'start' : 'noteOn'](0, this.startOffset % this.buffer.duration); |
|
// Start visualizer. |
|
requestAnimationFrame(this.analyze.bind(this)); |
|
} |
|
this.isPlaying = !this.isPlaying; |
|
return this; |
|
}, |
|
|
|
analyze : function(){ |
|
this.analyser.smoothingTimeConstant = this.smoothing; |
|
this.analyser.fftSize = this.fftSize; |
|
|
|
// Get the frequency data from the currently playing music |
|
this.analyser.getByteFrequencyData(this.freqs); |
|
this.analyser.getByteTimeDomainData(this.times); |
|
|
|
if (this.isPlaying) { |
|
requestAnimationFrame(this.analyze.bind(this)); |
|
} |
|
return this; |
|
}, |
|
|
|
drawDebug : function() { |
|
var width = Math.floor(1/this.freqs.length, 10); |
|
var canvas = document.querySelector('canvas'); |
|
var drawContext = canvas.getContext('2d'); |
|
var peakVal; |
|
canvas.width = window.innerWidth; |
|
canvas.height = window.innerHeight; |
|
// Draw the frequency domain chart. |
|
for (var i = 0; i < this.analyser.frequencyBinCount; i++) { |
|
var value = this.freqs[i]; |
|
var percent = value / 256; |
|
var height = canvas.height * percent; |
|
var offset = canvas.height - height - 1; |
|
var barWidth = canvas.width/this.analyser.frequencyBinCount; |
|
var hue = i/this.analyser.frequencyBinCount * 360; |
|
drawContext.fillStyle = 'hsl(' + hue + ', 100%, 50%)'; |
|
drawContext.fillRect(i * barWidth, offset, barWidth, height); |
|
} |
|
|
|
//console.log(this.times[this.analyser.frequencyBinCount /2] / 256); |
|
// Draw the time domain chart. |
|
// for (var i = 0; i < this.analyser.frequencyBinCount; i++) { |
|
|
|
|
|
// var height = height * percent; |
|
// var offset = height / 2; |
|
// var barWidth = width/this.analyser.frequencyBinCount; |
|
// drawContext.fillStyle = 'black'; |
|
// drawContext.fillRect(i * barWidth, offset, 1, 2); |
|
// } |
|
|
|
|
|
if (this.isPlaying) { |
|
requestAnimationFrame(this.draw.bind(this)); |
|
} |
|
return this; |
|
}, |
|
|
|
getFrequencyLevel : function(freq) { |
|
var nyquist = SONO.ctx.sampleRate/2; |
|
var index = Math.round(freq/nyquist * this.freqs.length); |
|
return this.freqs[index]; |
|
}, |
|
|
|
//@TODO need to create custom event here |
|
onBeat : function(){ |
|
this.gotBeat = true; |
|
}, |
|
|
|
//@TODO this needs method some math love |
|
detectBeat : function(){ |
|
if (this.aveLevel > this.beatCutOff && this.aveLevel > this.BEAT_MIN){ |
|
this.onBeat(); |
|
this.beatCutOff = this.aveLevel *1.1; |
|
this.beatTime = 0; |
|
}//else{ |
|
// if (beatTime <= ControlsHandler.audioParams.beatHoldTime){ |
|
// beatTime ++; |
|
// }else{ |
|
// beatCutOff *= ControlsHandler.audioParams.beatDecayRate; |
|
// beatCutOff = Math.max(this.beatCutOff,this.BEAT_MIN); |
|
// } |
|
// } |
|
// bpmTime = (new Date().getTime() - bpmStart)/msecsAvg; |
|
} |
|
|
|
}; |
|
|
|
Object.defineProperties(SONO.Visualizer.prototype, { |
|
|
|
aveLevel : { |
|
get : function(){ |
|
var levelsData = new Array(this.levelsCount); |
|
var sum; |
|
//normalize levelsData from freqByteData |
|
for(var i = 0; i < this.levelsCount; i++) { |
|
sum = 0; |
|
for(var j = 0; j < this.levelBins; j++) { |
|
sum += this.freqs[(i * this.levelBins) + j]; |
|
} |
|
levelsData[i] = sum / this.levelBins / 256; //freqData maxs at 256 |
|
|
|
//adjust for the fact that lower levels are percieved more quietly |
|
//make lower levels smaller |
|
//levelsData[i] *= 1 + (i/levelsCount)/2; |
|
} |
|
//TODO - cap levels at 1? |
|
|
|
//GET AVG LEVEL |
|
sum = 0; |
|
for(var j = 0; j < this.levelsCount; j++) { |
|
sum += levelsData[j]; |
|
} |
|
return sum / this.levelsCount; |
|
} |
|
}, |
|
|
|
beatCutOff : { |
|
value : 0, |
|
writable : true, |
|
configurable : true, |
|
enumerable : true |
|
}, |
|
BEAT_MIN : { |
|
value : 0.15, // defaults a volume less than this is no beat |
|
writable : true, |
|
configurable : true, |
|
enumerable : true |
|
} |
|
}); |