351 lines
13 KiB
JavaScript
351 lines
13 KiB
JavaScript
|
/**
|
||
|
* Web音频数据可视化模块
|
||
|
* @author Margox
|
||
|
* @version 0.0.1
|
||
|
*/
|
||
|
|
||
|
(function(factory){
|
||
|
|
||
|
/*
|
||
|
* 添加UMD支持
|
||
|
*/
|
||
|
|
||
|
if (typeof exports === 'object') {
|
||
|
module.exports = factory();
|
||
|
} else if (typeof define === 'function' && define.amd) {
|
||
|
define(factory);
|
||
|
} else {
|
||
|
window.Vudio = factory();
|
||
|
}
|
||
|
|
||
|
})(function() {
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
// 默认参数
|
||
|
var __default_option = {
|
||
|
effect : 'waveform',
|
||
|
accuracy : 128,
|
||
|
width : 256,
|
||
|
height : 100,
|
||
|
waveform : {
|
||
|
maxHeight : 80,
|
||
|
minHeight : 1,
|
||
|
spacing : 1,
|
||
|
color : '#f00',
|
||
|
shadowBlur : 0,
|
||
|
shadowColor : '#f00',
|
||
|
fadeSide : true,
|
||
|
horizontalAlign : 'center',
|
||
|
verticalAlign : 'middle',
|
||
|
prettify : true
|
||
|
},
|
||
|
lighting : {
|
||
|
maxHeight : 80,
|
||
|
lineWidth: 0,
|
||
|
color : '#f00',
|
||
|
shadowBlur : 0,
|
||
|
shadowColor : '#f00',
|
||
|
fadeSide : true,
|
||
|
horizontalAlign : 'center',
|
||
|
verticalAlign : 'middle'
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* 构造函数
|
||
|
* @param {object} audioSource HTMLAudioSource/MediaStream
|
||
|
* @param {object} canvasElement HTMLCanvasElement
|
||
|
* @param {object} option 可选配置参数
|
||
|
*/
|
||
|
function Vudio(audioSource, canvasElement, option) {
|
||
|
|
||
|
if (['[object HTMLAudioSource]', '[object HTMLAudioElement]', '[object MediaStream]'].indexOf(Object.prototype.toString.call(audioSource)) === -1) {
|
||
|
throw new TypeError('Invaild Audio Source');
|
||
|
}
|
||
|
|
||
|
if (Object.prototype.toString.call(canvasElement) !== '[object HTMLCanvasElement]') {
|
||
|
throw new TypeError('Invaild Canvas Element');
|
||
|
}
|
||
|
|
||
|
this.audioSrc = audioSource;
|
||
|
this.canvasEle = canvasElement;
|
||
|
this.option = __mergeOption(__default_option, option);
|
||
|
this.meta = {};
|
||
|
|
||
|
this.stat = 0;
|
||
|
this.freqByteData = null;
|
||
|
|
||
|
this.__init();
|
||
|
|
||
|
}
|
||
|
|
||
|
// private functions
|
||
|
function __mergeOption() {
|
||
|
|
||
|
var __result = {}
|
||
|
|
||
|
Array.prototype.forEach.call(arguments, function(argument) {
|
||
|
|
||
|
var __prop;
|
||
|
var __value;
|
||
|
|
||
|
for (__prop in argument) {
|
||
|
if (Object.prototype.hasOwnProperty.call(argument, __prop)) {
|
||
|
if (Object.prototype.toString.call(argument[__prop]) === '[object Object]') {
|
||
|
__result[__prop] = __mergeOption(__result[__prop], argument[__prop]);
|
||
|
} else {
|
||
|
__result[__prop] = argument[__prop];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
});
|
||
|
|
||
|
return __result;
|
||
|
|
||
|
}
|
||
|
|
||
|
Vudio.prototype = {
|
||
|
|
||
|
__init : function() {
|
||
|
|
||
|
var audioContext = new (window.AudioContext || window.webkitAudioContext || window.mozAudioContext),
|
||
|
source = Object.prototype.toString.call(this.audioSrc) !== '[object MediaStream]' ? audioContext.createMediaElementSource(this.audioSrc) : audioContext.createMediaStreamSource(this.audioSrc),
|
||
|
dpr = window.devicePixelRatio || 1;
|
||
|
|
||
|
this.analyser = audioContext.createAnalyser();
|
||
|
this.meta.spr = audioContext.sampleRate;
|
||
|
|
||
|
source.connect(this.analyser);
|
||
|
this.analyser.fftSize = this.option.accuracy * 2;
|
||
|
this.analyser.connect(audioContext.destination);
|
||
|
|
||
|
this.freqByteData = new Uint8Array(this.analyser.frequencyBinCount);
|
||
|
this.context2d = this.canvasEle.getContext('2d');
|
||
|
this.width = this.option.width;
|
||
|
this.height = this.option.height;
|
||
|
|
||
|
// ready for HD screen
|
||
|
this.context2d.canvas.width = this.width * dpr;
|
||
|
this.context2d.canvas.height = this.height * dpr;
|
||
|
this.context2d.scale(dpr, dpr);
|
||
|
|
||
|
},
|
||
|
|
||
|
__rebuildData : function (freqByteData, horizontalAlign) {
|
||
|
|
||
|
var __freqByteData;
|
||
|
|
||
|
if (horizontalAlign === 'center') {
|
||
|
__freqByteData = [].concat(
|
||
|
Array.from(freqByteData).reverse().splice(this.option.accuracy / 2, this.option.accuracy / 2),
|
||
|
Array.from(freqByteData).splice(0, this.option.accuracy / 2)
|
||
|
);
|
||
|
} else if (horizontalAlign === 'left') {
|
||
|
__freqByteData = freqByteData;
|
||
|
} else if (horizontalAlign === 'right') {
|
||
|
__freqByteData = Array.from(freqByteData).reverse();
|
||
|
} else {
|
||
|
__freqByteData = [].concat(
|
||
|
Array.from(freqByteData).reverse().splice(this.option.accuracy / 2, this.option.accuracy / 2),
|
||
|
Array.from(freqByteData).splice(0, this.option.accuracy / 2)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return __freqByteData;
|
||
|
|
||
|
},
|
||
|
|
||
|
__animate : function() {
|
||
|
|
||
|
if (this.stat === 1) {
|
||
|
this.analyser.getByteFrequencyData(this.freqByteData);
|
||
|
(typeof this.__effects()[this.option.effect] === 'function') && this.__effects()[this.option.effect](this.freqByteData);
|
||
|
requestAnimationFrame(this.__animate.bind(this));
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
__testFrame : function() {
|
||
|
this.analyser.getByteFrequencyData(this.freqByteData);
|
||
|
(typeof this.__effects()[this.option.effect] === 'function') && this.__effects()[this.option.effect](this.freqByteData);
|
||
|
},
|
||
|
|
||
|
// effect functions
|
||
|
__effects : function() {
|
||
|
|
||
|
var __that = this;
|
||
|
|
||
|
return {
|
||
|
|
||
|
lighting : function(freqByteData) {
|
||
|
|
||
|
var __lightingOption = __that.option.lighting;
|
||
|
var __freqByteData = __that.__rebuildData(freqByteData, __lightingOption.horizontalAlign);
|
||
|
var __maxHeight = __lightingOption.maxHeight / 2;
|
||
|
var __isStart = true, __fadeSide = true, __x, __y;
|
||
|
|
||
|
if (__lightingOption.horizontalAlign !== 'center') {
|
||
|
__fadeSide = false;
|
||
|
}
|
||
|
|
||
|
// clear canvas
|
||
|
__that.context2d.clearRect(0, 0, __that.width, __that.height);
|
||
|
|
||
|
// draw lighting
|
||
|
__that.context2d.lineWidth = __lightingOption.lineWidth;
|
||
|
__that.context2d.strokeStyle = __lightingOption.color;
|
||
|
__that.context2d.beginPath();
|
||
|
__freqByteData.forEach(function(value, index) {
|
||
|
|
||
|
__x = __that.width / __that.option.accuracy * index;
|
||
|
__y = value / 256 * __maxHeight;
|
||
|
|
||
|
if (__lightingOption.verticalAlign === 'middle') {
|
||
|
__y = (__that.height - value) / 2 - __maxHeight / 2;
|
||
|
} else if (__lightingOption.verticalAlign === 'bottom') {
|
||
|
__y = __that.height - value;
|
||
|
} else if (__lightingOption.verticalAlign === 'top') {
|
||
|
__y = value;
|
||
|
} else {
|
||
|
__y = (__that.height - value) / 2 - __maxHeight / 2;
|
||
|
}
|
||
|
|
||
|
if (__isStart) {
|
||
|
__that.context2d.moveTo(__x, __y);
|
||
|
__isStart = false;
|
||
|
} else {
|
||
|
__that.context2d.lineTo(__x, __y);
|
||
|
}
|
||
|
|
||
|
});
|
||
|
__that.context2d.stroke();
|
||
|
|
||
|
},
|
||
|
|
||
|
waveform : function (freqByteData) {
|
||
|
|
||
|
var __waveformOption = __that.option.waveform;
|
||
|
var __fadeSide = __waveformOption.fadeSide;
|
||
|
var __prettify = __waveformOption.prettify;
|
||
|
var __freqByteData = __that.__rebuildData(freqByteData, __waveformOption.horizontalAlign);
|
||
|
var __maxHeight, __width, __height, __left, __top, __color, __linearGradient, __pos;
|
||
|
|
||
|
if (__waveformOption.horizontalAlign !== 'center') {
|
||
|
__fadeSide = false;
|
||
|
__prettify = false;
|
||
|
}
|
||
|
|
||
|
// clear canvas
|
||
|
__that.context2d.clearRect(0, 0, __that.width, __that.height);
|
||
|
|
||
|
// draw waveform
|
||
|
__freqByteData.forEach(function(value, index){
|
||
|
|
||
|
__width = (__that.width - __that.option.accuracy * __waveformOption.spacing) / __that.option.accuracy;
|
||
|
__left = index * (__width + __waveformOption.spacing);
|
||
|
__waveformOption.spacing !== 1 && (__left += __waveformOption.spacing / 2);
|
||
|
|
||
|
if (__prettify) {
|
||
|
if (index <= __that.option.accuracy / 2) {
|
||
|
__maxHeight = (1 - (__that.option.accuracy / 2 - 1 - index) / ( __that.option.accuracy / 2)) * __waveformOption.maxHeight;
|
||
|
} else {
|
||
|
__maxHeight = (1 - (index - __that.option.accuracy / 2) / ( __that.option.accuracy / 2)) * __waveformOption.maxHeight;
|
||
|
}
|
||
|
} else {
|
||
|
__maxHeight = __waveformOption.maxHeight;
|
||
|
}
|
||
|
|
||
|
__height = value / 256 * __maxHeight;
|
||
|
__height = __height < __waveformOption.minHeight ? __waveformOption.minHeight : __height;
|
||
|
|
||
|
if (__waveformOption.verticalAlign === 'middle') {
|
||
|
__top = (__that.height - __height) / 2;
|
||
|
} else if (__waveformOption.verticalAlign === 'top') {
|
||
|
__top = 0;
|
||
|
} else if (__waveformOption.verticalAlign === 'bottom') {
|
||
|
__top = __that.height - __height;
|
||
|
} else {
|
||
|
__top = (__that.height - __height) / 2;
|
||
|
}
|
||
|
|
||
|
__color = __waveformOption.color;
|
||
|
|
||
|
if (__color instanceof Array) {
|
||
|
|
||
|
__linearGradient = __that.context2d.createLinearGradient(
|
||
|
__left,
|
||
|
__top,
|
||
|
__left,
|
||
|
__top + __height
|
||
|
);
|
||
|
|
||
|
__color.forEach(function(color, index) {
|
||
|
if (color instanceof Array) {
|
||
|
__pos = color[0];
|
||
|
color = color[1];
|
||
|
} else if (index === 0 || index === __color.length - 1) {
|
||
|
__pos = index / (__color.length - 1);
|
||
|
} else {
|
||
|
__pos = index / __color.length + 0.5 / __color.length;
|
||
|
}
|
||
|
__linearGradient.addColorStop(__pos, color);
|
||
|
});
|
||
|
|
||
|
__that.context2d.fillStyle = __linearGradient;
|
||
|
|
||
|
} else {
|
||
|
__that.context2d.fillStyle = __color;
|
||
|
}
|
||
|
|
||
|
if (__waveformOption.shadowBlur > 0) {
|
||
|
__that.context2d.shadowBlur = __waveformOption.shadowBlur;
|
||
|
__that.context2d.shadowColor = __waveformOption.shadowColor;
|
||
|
}
|
||
|
|
||
|
if (__fadeSide) {
|
||
|
if (index <= __that.option.accuracy / 2) {
|
||
|
__that.context2d.globalAlpha = 1 - (__that.option.accuracy / 2 - 1 - index) / ( __that.option.accuracy / 2);
|
||
|
} else {
|
||
|
__that.context2d.globalAlpha = 1 - (index - __that.option.accuracy / 2) / ( __that.option.accuracy / 2);
|
||
|
}
|
||
|
} else {
|
||
|
__that.context2d.globalAlpha = 1;
|
||
|
}
|
||
|
|
||
|
__that.context2d.fillRect(__left, __top, __width, __height);
|
||
|
|
||
|
});
|
||
|
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
},
|
||
|
|
||
|
// 开始
|
||
|
dance : function() {
|
||
|
if (this.stat === 0) {
|
||
|
this.stat = 1;
|
||
|
this.__animate();
|
||
|
}
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
// 暂停
|
||
|
pause : function() {
|
||
|
this.stat = 0;
|
||
|
return this;
|
||
|
},
|
||
|
|
||
|
// 改变参数
|
||
|
setOption : function(option) {
|
||
|
this.option = __mergeOption(this.option, option);
|
||
|
}
|
||
|
|
||
|
};
|
||
|
|
||
|
return Vudio;
|
||
|
|
||
|
});
|