mirror of
https://github.com/Yetangitu/ampache
synced 2025-10-03 09:49:30 +02:00
531 lines
No EOL
13 KiB
JavaScript
531 lines
No EOL
13 KiB
JavaScript
//UberViz AudioHandler
|
|
//Handles Audio loading and Playback
|
|
//Handles Audio Analysis + publishes audio data
|
|
//Handles Tap BPM
|
|
|
|
var AudioHandler = function() {
|
|
|
|
//PUBLIC/////////////
|
|
var waveData = []; //waveform - from 0 - 1 . no sound is 0.5. Array [binCount]
|
|
var levelsData = []; //levels of each frequecy - from 0 - 1 . no sound is 0. Array [levelsCount]
|
|
var volume = 0; // averaged normalized level from 0 - 1
|
|
var bpmTime = 0; // bpmTime ranges from 0 to 1. 0 = on beat. Based on tap bpm
|
|
var ratedBPMTime = 550;//time between beats (msec) multiplied by BPMRate
|
|
var levelHistory = []; //last 256 ave norm levels
|
|
var bpmStart; //FIXME
|
|
|
|
var BEAT_HOLD_TIME = 40; //num of frames to hold a beat
|
|
var BEAT_DECAY_RATE = 0.98;
|
|
var BEAT_MIN = 0.15; //level less than this is no beat
|
|
|
|
//BPM STUFF
|
|
var count = 0;
|
|
var msecsFirst = 0;
|
|
var msecsPrevious = 0;
|
|
var msecsAvg = 633; //time between beats (msec)
|
|
|
|
var timer;
|
|
var gotBeat = false;
|
|
|
|
var debugCtx;
|
|
var debugW = 250;
|
|
var debugH = 95;
|
|
var chartW = 220;
|
|
var chartH = 95;
|
|
var aveBarWidth = 30;
|
|
var bpmHeight = debugH - chartH;
|
|
var debugSpacing = 2;
|
|
var gradient;
|
|
var gainNode;
|
|
var filter1;
|
|
var filter2;
|
|
var filter3;
|
|
var filter4;
|
|
|
|
var freqByteData; //bars - bar data is from 0 - 256 in 512 bins. no sound is 0;
|
|
var timeByteData; //waveform - waveform data is from 0-256 for 512 bins. no sound is 128.
|
|
var levelsCount = 16; //should be factor of 512
|
|
|
|
var binCount; //512
|
|
var levelBins;
|
|
|
|
var isPlayingAudio = false;
|
|
|
|
var beatCutOff = 0;
|
|
var beatTime = 0;
|
|
|
|
var source;
|
|
var buffer;
|
|
var audioBuffer;
|
|
var dropArea;
|
|
var processor;
|
|
var analyser;
|
|
|
|
var high = 0;
|
|
|
|
|
|
function init() {
|
|
|
|
//EVENT HANDLERS
|
|
events.on("update", update);
|
|
|
|
processor = audioContext.createScriptProcessor(2048 , 1 , 1 );
|
|
|
|
analyser = audioContext.createAnalyser();
|
|
analyser.smoothingTimeConstant = 0.3; //smooths out bar chart movement over time
|
|
analyser.fftSize = 1024;
|
|
analyser.connect(audioContext.destination);
|
|
binCount = analyser.frequencyBinCount; // = 512
|
|
|
|
initEqualizer();
|
|
|
|
levelBins = Math.floor(binCount / levelsCount); //number of bins in each level
|
|
|
|
freqByteData = new Uint8Array(binCount);
|
|
timeByteData = new Uint8Array(binCount);
|
|
|
|
var length = 256;
|
|
for(var i = 0; i < length; i++) {
|
|
levelHistory.push(0);
|
|
}
|
|
|
|
//INIT DEBUG DRAW
|
|
var canvas = document.getElementById("audioDebug");
|
|
debugCtx = canvas.getContext('2d');
|
|
debugCtx.width = debugW;
|
|
debugCtx.height = debugH;
|
|
debugCtx.fillStyle = "rgb(40, 40, 40)";
|
|
debugCtx.lineWidth=2;
|
|
debugCtx.strokeStyle = "rgb(255, 255, 255)";
|
|
$('#audioDebugCtx').hide();
|
|
|
|
gradient = debugCtx.createLinearGradient(0,0,0,256);
|
|
gradient.addColorStop(1,'#330000');
|
|
gradient.addColorStop(0.75,'#aa0000');
|
|
gradient.addColorStop(0.25,'#aaaa00');
|
|
gradient.addColorStop(0,'#aaaaaa');
|
|
|
|
//assume 120BPM
|
|
msecsAvg = 640;
|
|
timer = setInterval(onBMPBeat,msecsAvg);
|
|
|
|
|
|
|
|
}
|
|
|
|
function initEqualizer() {
|
|
gainNode = audioContext.createGain();
|
|
gainNode.gain.value = 1;
|
|
|
|
filter1 = audioContext.createBiquadFilter();
|
|
filter1.type = 5;
|
|
filter1.gain.value = null;
|
|
filter1.Q.value = 1; // Change Filter type to test
|
|
filter1.frequency.value = 80; // Change frequency to test
|
|
|
|
filter2 = audioContext.createBiquadFilter();
|
|
filter2.type = 5;
|
|
filter2.gain.value = 0;
|
|
filter2.Q.value = 1; // Change Filter type to test
|
|
filter2.frequency.value = 240; // Change frequency to test
|
|
|
|
filter3 = audioContext.createBiquadFilter();
|
|
filter3.type = 5;
|
|
filter3.gain.value = 0;
|
|
filter3.Q.value = 1; // Change Filter type to test
|
|
filter3.frequency.value = 750; // Change frequency to test
|
|
|
|
filter4 = audioContext.createBiquadFilter();
|
|
filter4.type = 5;
|
|
filter4.gain.value = 0;
|
|
filter4.Q.value = 1; // Change Filter type to test
|
|
filter4.frequency.value = 2200; // Change frequency to test
|
|
|
|
filter5 = audioContext.createBiquadFilter();
|
|
filter5.type = 5;
|
|
filter5.gain.value = 0;
|
|
filter5.Q.value = 1; // Change Filter type to test
|
|
filter5.frequency.value = 6000; // Change frequency to test
|
|
|
|
var sliderParams80Hz = {
|
|
'orientation': "vertical",
|
|
'range': "min",
|
|
'min': -30,
|
|
'max': 30,
|
|
'animate': true,
|
|
'step': 0.01,
|
|
'slide': function(event, ui) {
|
|
filter1.gain.value = ui.value;
|
|
|
|
},
|
|
'stop': function(event, ui) {
|
|
console.log(filter1.gain.value);
|
|
}
|
|
};
|
|
$('#filter80Hz').slider(sliderParams80Hz);
|
|
|
|
var sliderParams240Hz = {
|
|
'orientation': "vertical",
|
|
'range': "min",
|
|
'min': -30,
|
|
'max': 30,
|
|
'animate': true,
|
|
'step': 0.01,
|
|
'slide': function(event, ui) {
|
|
filter2.gain.value = ui.value;
|
|
|
|
},
|
|
'stop': function(event, ui) {
|
|
console.log(filter2.gain.value);
|
|
}
|
|
};
|
|
$('#filter240Hz').slider(sliderParams240Hz);
|
|
|
|
var sliderParams750Hz = {
|
|
'orientation': "vertical",
|
|
'range': "min",
|
|
'min': -30,
|
|
'max': 30,
|
|
'animate': true,
|
|
'step': 0.01,
|
|
'slide': function(event, ui) {
|
|
filter3.gain.value = ui.value;
|
|
|
|
},
|
|
'stop': function(event, ui) {
|
|
console.log(filter3.gain.value);
|
|
}
|
|
};
|
|
$('#filter750Hz').slider(sliderParams750Hz);
|
|
|
|
var sliderParams2200Hz = {
|
|
'orientation': "vertical",
|
|
'range': "min",
|
|
'min': -30,
|
|
'max': 30,
|
|
'animate': true,
|
|
'step': 0.01,
|
|
'slide': function(event, ui) {
|
|
filter4.gain.value = ui.value;
|
|
|
|
},
|
|
'stop': function(event, ui) {
|
|
console.log(filter4.gain.value);
|
|
}
|
|
};
|
|
$('#filter2200Hz').slider(sliderParams2200Hz);
|
|
|
|
var sliderParams6000Hz = {
|
|
'orientation': "vertical",
|
|
'range': "min",
|
|
'min': -30,
|
|
'max': 30,
|
|
'animate': true,
|
|
'step': 0.01,
|
|
'slide': function(event, ui) {
|
|
filter5.gain.value = ui.value;
|
|
|
|
},
|
|
'stop': function(event, ui) {
|
|
console.log(filter5.gain.value);
|
|
}
|
|
};
|
|
$('#filter6000Hz').slider(sliderParams6000Hz);
|
|
}
|
|
|
|
function initSound(){
|
|
source = audioContext.createBufferSource();
|
|
source.connect(analyser);
|
|
}
|
|
|
|
function startSound() {
|
|
source.buffer = audioBuffer;
|
|
source.loop = true;
|
|
source.start();
|
|
isPlayingAudio = true;
|
|
//startViz();
|
|
}
|
|
|
|
function loadMediaSource(mediaElement) {
|
|
if (mediaElement != undefined) {
|
|
if (mediaSource == null) {
|
|
mediaSource = audioContext.createMediaElementSource(mediaElement);
|
|
}
|
|
source = mediaSource;
|
|
source.connect(analyser);
|
|
analyser.connect(gainNode);
|
|
gainNode.connect(filter1);
|
|
filter1.connect(filter2);
|
|
filter2.connect(filter3);
|
|
filter3.connect(filter4);
|
|
filter4.connect(filter5);
|
|
filter5.connect(audioContext.destination);
|
|
isPlayingAudio = true;
|
|
}
|
|
}
|
|
|
|
function stopSound(){
|
|
isPlayingAudio = false;
|
|
if (source) {
|
|
source.disconnect();
|
|
}
|
|
debugCtx.clearRect(0, 0, debugW, debugH);
|
|
}
|
|
|
|
function onShowDebug(){
|
|
if (ControlsHandler.audioParams.showDebug){
|
|
$('#audioDebug').show();
|
|
}else{
|
|
$('#audioDebug').hide();
|
|
}
|
|
|
|
}
|
|
|
|
function onBeat(){
|
|
//console.log("BEAT");
|
|
// TweenLite.to(this, 1, {debugLum:, ease:Power2.easeOut});
|
|
// TweenMax.to(this, 1, css:{ color: "FFFFFF" } );
|
|
|
|
//experimental combined beat + bpm mode
|
|
gotBeat = true;
|
|
|
|
if (ControlsHandler.audioParams.bpmMode) return;
|
|
|
|
events.emit("onBeat");
|
|
}
|
|
|
|
function onBMPBeat(){
|
|
//console.log("onBMPBeat");
|
|
bpmStart = new Date().getTime();
|
|
|
|
if (!ControlsHandler.audioParams.bpmMode) return;
|
|
|
|
//only fire bpm beat if there was an on onBeat in last timeframe
|
|
//experimental combined beat + bpm mode
|
|
//if (gotBeat){
|
|
NeonShapes.onBPMBeat();
|
|
//GoldShapes.onBPMBeat();
|
|
gotBeat = false;
|
|
//}
|
|
|
|
}
|
|
|
|
|
|
//called every frame
|
|
//update published viz data
|
|
function update(){
|
|
|
|
|
|
//console.log("audio.update");
|
|
|
|
if (!isPlayingAudio) return;
|
|
|
|
//GET DATA
|
|
analyser.getByteFrequencyData(freqByteData); //<-- bar chart
|
|
analyser.getByteTimeDomainData(timeByteData); // <-- waveform
|
|
|
|
//normalize waveform data
|
|
for(var i = 0; i < binCount; i++) {
|
|
waveData[i] = ((timeByteData[i] - 128) /128 )* ControlsHandler.audioParams.volSens;
|
|
}
|
|
//TODO - cap levels at 1 and -1 ?
|
|
|
|
//normalize levelsData from freqByteData
|
|
for(var i = 0; i < levelsCount; i++) {
|
|
var sum = 0;
|
|
for(var j = 0; j < levelBins; j++) {
|
|
sum += freqByteData[(i * levelBins) + j];
|
|
}
|
|
levelsData[i] = sum / levelBins/256 * ControlsHandler.audioParams.volSens; //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
|
|
var sum = 0;
|
|
for(var j = 0; j < levelsCount; j++) {
|
|
sum += levelsData[j];
|
|
}
|
|
|
|
volume = sum / levelsCount;
|
|
|
|
// high = Math.max(high,level);
|
|
levelHistory.push(volume);
|
|
levelHistory.shift(1);
|
|
|
|
//BEAT DETECTION
|
|
if (volume > beatCutOff && volume > BEAT_MIN){
|
|
onBeat();
|
|
beatCutOff = volume *1.1;
|
|
beatTime = 0;
|
|
}else{
|
|
if (beatTime <= ControlsHandler.audioParams.beatHoldTime){
|
|
beatTime ++;
|
|
}else{
|
|
beatCutOff *= ControlsHandler.audioParams.beatDecayRate;
|
|
beatCutOff = Math.max(beatCutOff,BEAT_MIN);
|
|
}
|
|
}
|
|
|
|
|
|
bpmTime = (new Date().getTime() - bpmStart)/msecsAvg;
|
|
//trace(bpmStart);
|
|
|
|
if (ControlsHandler.audioParams.showDebug) debugDraw();
|
|
}
|
|
|
|
function debugDraw(){
|
|
|
|
debugCtx.clearRect(0, 0, debugW, debugH);
|
|
//draw chart bkgnd
|
|
debugCtx.fillStyle = "#000";
|
|
debugCtx.fillRect(0,0,debugW,debugH);
|
|
|
|
//DRAW BAR CHART
|
|
// Break the samples up into bars
|
|
var barWidth = chartW / levelsCount;
|
|
debugCtx.fillStyle=gradient;
|
|
for(var i = 0; i < levelsCount; i++) {
|
|
debugCtx.fillRect(i * barWidth, chartH, barWidth - debugSpacing, -levelsData[i]*chartH);
|
|
}
|
|
|
|
//DRAW AVE LEVEL + BEAT COLOR
|
|
if (beatTime < 6){
|
|
debugCtx.fillStyle="#FFF";
|
|
}
|
|
debugCtx.fillRect(chartW, chartH, aveBarWidth, -volume*chartH);
|
|
|
|
//DRAW CUT OFF
|
|
debugCtx.beginPath();
|
|
debugCtx.moveTo(chartW , chartH - beatCutOff*chartH);
|
|
debugCtx.lineTo(chartW + aveBarWidth, chartH - beatCutOff*chartH);
|
|
debugCtx.stroke();
|
|
|
|
//DRAW WAVEFORM
|
|
debugCtx.beginPath();
|
|
for(var i = 0; i < binCount; i++) {
|
|
debugCtx.lineTo(i/binCount*chartW, waveData[i]*chartH/2 + chartH/2);
|
|
}
|
|
debugCtx.stroke();
|
|
|
|
//DRAW BPM
|
|
if (bpmHeight > 0) {
|
|
var bpmMaxSize = bpmHeight;
|
|
var size = bpmMaxSize - bpmTime*bpmMaxSize;
|
|
debugCtx.fillStyle="#020";
|
|
debugCtx.fillRect(0,chartH, bpmMaxSize, bpmMaxSize);
|
|
debugCtx.fillStyle="#0F0";
|
|
debugCtx.fillRect((bpmMaxSize - size)/2,chartH + (bpmMaxSize - size)/2, size, size);
|
|
}
|
|
}
|
|
|
|
function onTap() {
|
|
|
|
console.log("ontap");
|
|
|
|
clearInterval(timer);
|
|
|
|
timeSeconds = new Date();
|
|
msecs = timeSeconds.getTime();
|
|
|
|
//after 2 seconds, new tap counts as a new sequnce
|
|
if ((msecs - msecsPrevious) > 2000){
|
|
count = 0;
|
|
}
|
|
|
|
if (count === 0){
|
|
console.log("First Beat");
|
|
msecsFirst = msecs;
|
|
count = 1;
|
|
}else{
|
|
bpmAvg = 60000 * count / (msecs - msecsFirst);
|
|
msecsAvg = (msecs - msecsFirst)/count;
|
|
count++;
|
|
console.log("bpm: " + Math.round(bpmAvg * 100) / 100 + " , taps: " + count + " , msecs: " + msecsAvg);
|
|
onBMPBeat();
|
|
clearInterval(timer);
|
|
timer = setInterval(onBMPBeat,msecsAvg);
|
|
}
|
|
msecsPrevious = msecs;
|
|
}
|
|
|
|
function onChangeBPMRate(){
|
|
|
|
//change rate without losing current beat time
|
|
|
|
//get ratedBPMTime from real bpm
|
|
switch(ControlsHandler.audioParams.bpmRate)
|
|
{
|
|
case -3:
|
|
ratedBPMTime = msecsAvg *8;
|
|
break;
|
|
case -2:
|
|
ratedBPMTime = msecsAvg *4;
|
|
break;
|
|
case -1:
|
|
ratedBPMTime = msecsAvg *2;
|
|
break;
|
|
case 0:
|
|
ratedBPMTime = msecsAvg;
|
|
break;
|
|
case 1:
|
|
ratedBPMTime = msecsAvg /2;
|
|
break;
|
|
case 2:
|
|
ratedBPMTime = msecsAvg /4;
|
|
break;
|
|
case 3:
|
|
ratedBPMTime = msecsAvg /8;
|
|
break;
|
|
case 4:
|
|
ratedBPMTime = msecsAvg /16;
|
|
break;
|
|
}
|
|
|
|
//console.log("ratedBPMTime: " + ratedBPMTime);
|
|
|
|
|
|
//get distance to next beat
|
|
bpmTime = (new Date().getTime() - bpmStart)/msecsAvg;
|
|
|
|
|
|
timeToNextBeat = ratedBPMTime - (new Date().getTime() - bpmStart);
|
|
|
|
//set one-off timer for that
|
|
clearInterval(timer);
|
|
timer = setInterval(onFirstBPM,timeToNextBeat);
|
|
|
|
|
|
//set timer for new beat rate
|
|
|
|
|
|
}
|
|
|
|
function onFirstBPM(){
|
|
clearInterval(timer);
|
|
timer = setInterval(onBMPBeat,ratedBPMTime);
|
|
}
|
|
|
|
// function toggleBPMMode(tog){
|
|
// console.log("PP");
|
|
// }
|
|
|
|
return {
|
|
onShowDebug:onShowDebug,
|
|
update:update,
|
|
init:init,
|
|
loadMediaSource:loadMediaSource,
|
|
onTap:onTap,
|
|
onChangeBPMRate:onChangeBPMRate,
|
|
getLevelsData: function() { return levelsData;},
|
|
getVolume: function() { return volume;},
|
|
getBPMTime: function() { return bpmTime;},
|
|
|
|
};
|
|
|
|
}(); |