vue-waveform
Version:
waveform audio player wavesurfer -waveform js html 音频audio波形图
947 lines (845 loc) • 33.3 kB
JavaScript
import Analysis from './analysis'
// import { extend, debounce } from './util'
import { extend } from './util'
import Tween from './animation'
function LibFFT(bufferSize){
var FFT_N_LOG,FFT_N,MINY;
var real, imag, sintable, costable;
var bitReverse;
var FFT_Fn=function(bufferSize) {//bufferSize只能取值2的n次方
FFT_N_LOG=Math.round(Math.log(bufferSize)/Math.log(2));
FFT_N = 1 << FFT_N_LOG;
MINY = ((FFT_N << 2) * Math.sqrt(2));
real = [];
imag = [];
sintable = [0];
costable = [0];
bitReverse = [];
var i, j, k, reve;
for (i = 0; i < FFT_N; i++) {
k = i;
for (j = 0, reve = 0; j != FFT_N_LOG; j++) {
reve <<= 1;
reve |= (k & 1);
k >>>= 1;
}
bitReverse[i] = reve;
}
var theta, dt = 2 * Math.PI / FFT_N;
for (i = (FFT_N >> 1) - 1; i > 0; i--) {
theta = i * dt;
costable[i] = Math.cos(theta);
sintable[i] = Math.sin(theta);
}
}
/*
用于频谱显示的快速傅里叶变换
inBuffer 输入FFT_N个实数,返回 FFT_N/2个输出值(复数模的平方)。
*/
var getModulus=function(inBuffer) {
var i, j, k, ir, j0 = 1, idx = FFT_N_LOG - 1;
var cosv, sinv, tmpr, tmpi;
for (i = 0; i != FFT_N; i++) {
real[i] = inBuffer[bitReverse[i]];
imag[i] = 0;
}
for (i = FFT_N_LOG; i != 0; i--) {
for (j = 0; j != j0; j++) {
cosv = costable[j << idx];
sinv = sintable[j << idx];
for (k = j; k < FFT_N; k += j0 << 1) {
ir = k + j0;
tmpr = cosv * real[ir] - sinv * imag[ir];
tmpi = cosv * imag[ir] + sinv * real[ir];
real[ir] = real[k] - tmpr;
imag[ir] = imag[k] - tmpi;
real[k] += tmpr;
imag[k] += tmpi;
}
}
j0 <<= 1;
idx--;
}
j = FFT_N >> 1;
var outBuffer=new Float64Array(j);
/*
* 输出模的平方:
* for(i = 1; i <= j; i++)
* inBuffer[i-1] = real[i] * real[i] + imag[i] * imag[i];
*
* 如果FFT只用于频谱显示,可以"淘汰"幅值较小的而减少浮点乘法运算. MINY的值
* 和Spectrum.Y0,Spectrum.logY0对应.
*/
sinv = MINY;
cosv = -MINY;
for (i = j; i != 0; i--) {
tmpr = real[i];
tmpi = imag[i];
if (tmpr > cosv && tmpr < sinv && tmpi > cosv && tmpi < sinv)
outBuffer[i - 1] = 0;
else
outBuffer[i - 1] = Math.round(tmpr * tmpr + tmpi * tmpi);
}
return outBuffer;
}
FFT_Fn(bufferSize);
return {transform:getModulus,bufferSize:FFT_N};
};
let defaultOpt = {
WIDTH: 500,
HEIGHT: 300
}
let AudioContext = window.AudioContext || window.webkitAudioContext
const audioContext = new AudioContext()
const analyser = audioContext.createAnalyser()
let Set = {
lineCount:20
,position:0
,minHeight:1
,fallDuration:400
,stripeEnable:false
,mirrorEnable:true
,linear:[0,"#0ac",1,"#0ac"]
}
let originSet = {
scale:2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊
,fps:20 //绘制帧率,不可过高
,lineCount:30 //直方图柱子数量,数量的多少对性能影响不大,密集运算集中在FFT算法中
,widthRatio:0.6 //柱子线条宽度占比,为所有柱子占用整个视图宽度的比例,剩下的空白区域均匀插入柱子中间;默认值也基本相当于一根柱子占0.6,一根空白占0.4;设为1不留空白,当视图不足容下所有柱子时也不留空白
,spaceWidth:0 //柱子间空白固定基础宽度,柱子宽度自适应,当不为0时widthRatio无效,当视图不足容下所有柱子时将不会留空白,允许为负数,让柱子发生重叠
,minHeight:0 //柱子保留基础高度,position不为±1时应该保留点高度
,position:-1 //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比
,mirrorEnable:false //是否启用镜像,如果启用,视图宽度会分成左右两块,右边这块进行绘制,左边这块进行镜像(以中间这根柱子的中心进行镜像)
,stripeEnable:true //是否启用柱子顶上的峰值小横条,position不是-1时应当关闭,否则会很丑
,stripeHeight:3 //峰值小横条基础高度
,stripeMargin:6 //峰值小横条和柱子保持的基础距离
,fallDuration:1000 //柱子从最顶上下降到最底部最长时间ms
,stripeFallDuration:3500 //峰值小横条从最顶上下降到底部最长时间ms
//柱子颜色配置:[位置,css颜色,...] 位置: 取值0.0-1.0之间
,linear:[0,"rgba(0,187,17,1)",0.5,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"]
//峰值小横条渐变颜色配置,取值格式和linear一致,留空为柱子的渐变颜色
,stripeLinear:null
,shadowBlur:0 //柱子阴影基础大小,设为0不显示阴影,如果柱子数量太多时请勿开启,非常影响性能
,shadowColor:"#bbb" //柱子阴影颜色
,stripeShadowBlur:-1 //峰值小横条阴影基础大小,设为0不显示阴影,-1为柱子的大小,如果柱子数量太多时请勿开启,非常影响性能
,stripeShadowColor:"" //峰值小横条阴影颜色,留空为柱子的阴影颜色
,fullFreq:false //是否要绘制所有频率;默认false主要绘制5khz以下的频率,高频部分占比很少,此时不同的采样率对频谱显示几乎没有影响;设为true后不同采样率下显示的频谱是不一样的,因为 最大频率=采样率/2 会有差异
//当发生绘制时会回调此方法,参数为当前绘制的频率数据和采样率,可实现多个直方图同时绘制,只消耗一个input输入和计算时间
}
export default class Drawer {
constructor (options) {
this.opts = Object.assign({}, defaultOpt)
this.set = Object.assign(originSet, Set)
this.fft=LibFFT(1024);
//柱子所在高度
this.lastH=[];
//峰值小横条所在高度
this.stripesH=[];
this.tween = new Tween()
extend(this.opts, options || {})
analyser.fftSize = this.opts.fftSize
if (this.opts.type === 'line3') {
this.analysisLine = new Analysis({WIDTH: this.opts.WIDTH / 4, audioContext: audioContext, splitChannels: this.opts.splitChannels})
this.queue = []
this.masterArr = []
// this.execQueue()
} else {
this.analysisLine = new Analysis({WIDTH: this.opts.WIDTH, audioContext: audioContext, splitChannels: this.opts.splitChannels})
}
this.receive = this.warpperReceive(this.opts.type)
this.drawBar = this.drawBar.bind(this)
this.drawBar2 = this.drawBar2.bind(this)
this.drawLine = this.drawLine.bind(this)
this.animate = this.animate.bind(this)
this.mybuffer = undefined
this.animationStart = false
this.isOpen = false
}
setWidth(v, buffer) {
this.clear()
this.opts.WIDTH = v
}
review(buffer) { // 设置重新绘制
this.analysisLine = new Analysis({WIDTH: this.opts.WIDTH, audioContext: audioContext})
return this.receive(buffer)
}
destory() {
if (this.opts && this.opts.canvasCtx) {
this.opts.canvasCtx.clearRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
}
}
genLinear(ctx,colors,from,to){
var rtv=ctx.createLinearGradient(0,from,0,to);
for(var i=0;i<colors.length;){
rtv.addColorStop(colors[i++],colors[i++]);
};
return rtv;
}
input(pcmData,powerLevel,sampleRate){
var This=this;
This.sampleRate=sampleRate;
This.pcmData=pcmData;
This.pcmPos=0;
This.inputTime=Date.now();
This.schedule();
}
schedule(){
var This=this,set=This.set;
var interval=Math.floor(1000/set.fps);
if(!This.timer){
This.timer=setInterval(function(){
This.schedule();
},interval);
};
var now=Date.now();
var drawTime=This.drawTime||0;
if(now-This.inputTime>set.stripeFallDuration*1.3){
//超时没有输入,顶部横条已全部落下,干掉定时器
clearInterval(This.timer);
This.timer=0;
This.lastH=[];//重置高度再绘制一次,避免定时不准没到底就停了
This.stripesH=[];
This.drawHistogram(null,This.sampleRate);
return;
};
if(now-drawTime<interval){
//没到间隔时间,不绘制
return;
};
This.drawTime=now;
//调用FFT计算频率数据
var bufferSize=This.fft.bufferSize;
var pcm=This.pcmData;
var pos=This.pcmPos;
var arr=new Int16Array(bufferSize);
for(var i=0;i<bufferSize&&pos<pcm.length;i++,pos++){
arr[i]=pcm[pos];
};
This.pcmPos=pos;
var frequencyData=This.fft.transform(arr);
//推入绘制
This.drawHistogram(frequencyData,This.sampleRate);
}
addData(arraybuffer) {
let _this =this
audioContext.decodeAudioData(arraybuffer, function(buffer) {
this.drawBar()
})
}
warpperReceive(type) {
let _this = this
if (type === 'bar'|| type === 'bar2') {
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
_this.analysisBar(buffer, type)
resolve(buffer)
})
})
}
} else if (type === 'line') {
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
_this.analysisBar(buffer, type)
resolve(buffer)
})
})
}
} else if (type === 'line2') {
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
_this.mybuffer = buffer
let arr = _this.analysisLine.getPeaks(buffer)
_this.drawLine2(arr)
resolve(buffer)
})
})
}
} else if (type === 'line3') {
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
//_this.queue.push(buffer)
_this.drawLine3(buffer)
})
})
}
} else if (type === 'block') {
this.outcanvas = document.createElement('canvas')
this.outcanvas.width = this.opts.WIDTH
this.outcanvas.height = this.opts.HEIGHT / 2
this.octx = this.outcanvas.getContext('2d')
this.initAnimation()
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
_this.analysisBar(buffer, type)
})
})
}
} else if (type === 'his') {
return function (arraybuffer) {
return new Promise((resolve, reject) => {
audioContext.decodeAudioData(arraybuffer, function(buffer) {
_this.analysisBar(buffer, type)
resolve(buffer)
})
})
}
}
}
execQueue(time) {
if (this.queue.length === 0) {
setTimeout(this.execQueue.bind(this), time || 512)
} else {
let buffer = this.queue[0]
this.mybuffer = buffer
let peaks = this.analysisLine.getPeaks(buffer)
this.drawLine3(peaks)
this.queue.shift()
this.timeid = setTimeout(this.execQueue.bind(this, buffer.duration * 1000), time || 512)
}
}
analysisBar(buffer, type) {
this.audioBufferSourceNode = audioContext.createBufferSource()
this.audioBufferSourceNode.buffer = buffer
this.audioBufferSourceNode.connect(analyser)
//this.audioBufferSourceNode.connect(audioContext.destination)
this.audioBufferSourceNode.start(0)
this.bufferLength = analyser.frequencyBinCount
this.dataArray = new Uint8Array(this.bufferLength)
if (!this.isOpen) {
if (type === 'line') {
this.drawLine()
} else if (type === 'bar') {
this.drawBar()
} else if (type === 'bar2') {
this.drawBar2()
}else if (type === 'block') {
this.animate()
}
else if (type === 'his') {
var arr = buffer.getChannelData(0)
this.input(new Int16Array(arr.buffer),2,buffer.sampleRate)
}
this.isOpen = true
}
}
//动画初始化,获取analyserNode里的音频buffer
initAnimation() {
this.rt_array = []
this.rt_length = this.opts.WIDTH / 15
var grd = this.opts.canvasCtx.createLinearGradient(0, this.opts.WIDTH / 2 * 0.3, 0, this.opts.WIDTH / 2)
grd.addColorStop(0, '#8dbae9')
grd.addColorStop(0.3, '#65a6e9')
grd.addColorStop(1, '#378adf')
//每个柱形条的宽度,及柱形条宽度+间隔
var aw = this.opts.WIDTH / this.rt_length
var w = aw - 5
for (var i = 0; i < this.rt_length; i++) {
this.rt_array.push(new Retangle(w, 5, i * aw, this.opts.HEIGHT / 2, this.octx, grd))
}
// animate();
}
animate() {
if (!this) return
if (!this.animationStart) return
if (this.dataArray === null) return
this.opts.canvasCtx.clearRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.octx.clearRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
analyser.getByteFrequencyData(this.dataArray)
let bili = this.bufferLength / this.opts.WIDTH
for (var i = 0; i < this.rt_array.length; i++) {
var rt = this.rt_array[i]
rt.index = ('index' in rt) ? rt.index : ~~(rt.x * bili)
rt.update(this.dataArray[rt.index] * 0.48)
}
this.draw()
window.requestAnimationFrame(this.animate)
}
draw() {
this.opts.canvasCtx.drawImage(this.outcanvas, 0, 0)
this.opts.canvasCtx.save()
this.opts.canvasCtx.translate(this.opts.WIDTH / 2, this.opts.HEIGHT / 2)
this.opts.canvasCtx.rotate(Math.PI)
this.opts.canvasCtx.scale(-1, 1)
this.opts.canvasCtx.drawImage(this.outcanvas, (-this.opts.WIDTH) / 2, (-this.opts.HEIGHT) / 2)
this.opts.canvasCtx.restore()
this.opts.canvasCtx.fillStyle = 'rgba(0, 0, 0, 0)'
this.opts.canvasCtx.fillRect(0, this.opts.HEIGHT / 2, this.opts.WIDTH, this.opts.HEIGHT / 2)
}
drawHistogram(frequencyData,sampleRate = 48000){
var This=this,set= {
fallDuration: 400,
fps: 20,
fullFreq: false,
height: 100,
lineCount: 20,
linear: [0, '#0ac', 1, '#0ac'],
minHeight: 1,
mirrorEnable: true,
position: 0,
scale: 2,
shadowBlur: 0,
shadowColor: "#bbb",
spaceWidth: 0,
stripeEnable: false,
stripeFallDuration: 3500,
stripeHeight: 3,
stripeLinear: null,
stripeMargin: 6,
stripeShadowBlur: -1,
stripeShadowColor: "",
width:300,
widthRatio: 0.6
}
var ctx=This.opts.canvasCtx;
var scale=set.scale;
var width=set.width*scale;
var height=set.height*scale;
var lineCount=set.lineCount;
var bufferSize=This.fft.bufferSize;
//计算高度位置
var position=set.position;
var posAbs=Math.abs(set.position);
var originY=position==1?0:height;//y轴原点
var heightY=height;//最高的一边高度
if(posAbs<1){
heightY=heightY/2;
originY=heightY;
heightY=Math.floor(heightY*(1+posAbs));
originY=Math.floor(position>0?originY*(1-posAbs):originY*(1+posAbs));
};
var lastH=This.lastH;
var stripesH=This.stripesH;
var speed=Math.ceil(heightY/(set.fallDuration/(1000/set.fps)));
var stripeSpeed=Math.ceil(heightY/(set.stripeFallDuration/(1000/set.fps)));
var stripeMargin=set.stripeMargin*scale;
var Y0=1 << (Math.round(Math.log(bufferSize)/Math.log(2) + 3) << 1);
var logY0 = Math.log(Y0)/Math.log(10);
var dBmax=20*Math.log(0x7fff)/Math.log(10);
var fftSize=bufferSize/2,fftSize5k=fftSize;
if(!set.fullFreq){//非绘制所有频率时,计算5khz所在位置,8000采样率及以下最高只有4khz
fftSize5k=Math.min(fftSize,Math.floor(fftSize*5000/(sampleRate/2)));
}
var isFullFreq=fftSize5k==fftSize;
var line80=isFullFreq?lineCount:Math.round(lineCount*0.8);//80%的柱子位置
var fftSizeStep1=fftSize5k/line80;
var fftSizeStep2=isFullFreq?0:(fftSize-fftSize5k)/(lineCount-line80);
var fftIdx=0;
for(var i=0;i<lineCount;i++){
// !fullFreq 时不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。
var start=Math.ceil(fftIdx);
if(i<line80){
//5khz以下
fftIdx+=fftSizeStep1;
}else{
//5khz以上
fftIdx+=fftSizeStep2;
};
var end=Math.ceil(fftIdx); if(end==start)end++;
end=Math.min(end,fftSize);
//参考AudioGUI.java .drawHistogram方法
//查找当前频段的最大"幅值"
var maxAmp=0;
if(frequencyData){
for (var j=start; j<end; j++) {
maxAmp=Math.max(maxAmp,Math.abs(frequencyData[j]));
};
};
//计算音量
var dB= (maxAmp > Y0) ? Math.floor((Math.log(maxAmp)/Math.log(10) - logY0) * 17) : 0;
var h=heightY*Math.min(dB/dBmax,1);
//使柱子匀速下降
lastH[i]=(lastH[i]||0)-speed;
if(h<lastH[i]){h=lastH[i];};
if(h<0){h=0;};
lastH[i]=h;
var shi=stripesH[i]||0;
if(h&&h+stripeMargin>shi) {
stripesH[i]=h+stripeMargin;
}else{
//使峰值小横条匀速度下落
var sh =shi-stripeSpeed;
if(sh < 0){sh = 0;};
stripesH[i] = sh;
};
};
//开始绘制图形
ctx.clearRect(0,0,width,height);
var linear1=This.genLinear(ctx,set.linear,originY,originY-heightY);//上半部分的填充
var stripeLinear1=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY-heightY)||linear1;//上半部分的峰值小横条填充
var linear2=This.genLinear(ctx,set.linear,originY,originY+heightY);//下半部分的填充
var stripeLinear2=set.stripeLinear&&This.genLinear(ctx,set.stripeLinear,originY,originY+heightY)||linear2;//上半部分的峰值小横条填充
//计算柱子间距
var mirrorEnable=set.mirrorEnable;
var mirrorCount=mirrorEnable?lineCount*2-1:lineCount;//镜像柱子数量翻一倍-1根
var widthRatio=set.widthRatio;
var spaceWidth=set.spaceWidth*scale;
if(spaceWidth!=0){
widthRatio=(width-spaceWidth*(mirrorCount+1))/width;
};
for(var i=0;i<2;i++){
var lineFloat=Math.max(1*scale,(width*widthRatio)/mirrorCount);//柱子宽度至少1个单位
var lineWN=Math.floor(lineFloat),lineWF=lineFloat-lineWN;//提取出小数部分
var spaceFloat=(width-mirrorCount*lineFloat)/(mirrorCount+1);//均匀间隔,首尾都留空,可能为负数,柱子将发生重叠
if(spaceFloat>0 && spaceFloat<1){
widthRatio=1; spaceFloat=0; //不够一个像素,丢弃不绘制间隔,重新计算
}else break;
};
//绘制
var minHeight=set.minHeight*scale;
var XFloat=mirrorEnable?(width-lineWN)/2-spaceFloat:0;//镜像时,中间柱子位于正中心
for(var iMirror=0;iMirror<2;iMirror++){
if(iMirror){ ctx.save(); ctx.scale(-1,1); }
var xMirror=iMirror?width:0; //绘制镜像部分,不用drawImage(canvas)进行镜像绘制,提升兼容性(iOS微信小程序bug https://developers.weixin.qq.com/community/develop/doc/000aaca2148dc8a235a0fb8c66b000)
//绘制柱子
ctx.shadowBlur=set.shadowBlur*scale;
ctx.shadowColor=set.shadowColor;
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
xFloat+=spaceFloat;
x=Math.floor(xFloat)-xMirror;
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
h=Math.max(lastH[i],minHeight);
//绘制上半部分
if(originY!=0){
y=originY-h;
ctx.fillStyle=linear1;
ctx.fillRect(x, y, w, h);
};
//绘制下半部分
if(originY!=height){
ctx.fillStyle=linear2;
ctx.fillRect(x, originY, w, h);
};
xFloat+=w;
};
//绘制柱子顶上峰值小横条
if(set.stripeEnable){
var stripeShadowBlur=set.stripeShadowBlur;
ctx.shadowBlur=(stripeShadowBlur==-1?set.shadowBlur:stripeShadowBlur)*scale;
ctx.shadowColor=set.stripeShadowColor||set.shadowColor;
var stripeHeight=set.stripeHeight*scale;
for(var i=0,xFloat=XFloat,wFloat=0,x,y,w,h;i<lineCount;i++){
xFloat+=spaceFloat;
x=Math.floor(xFloat)-xMirror;
w=lineWN; wFloat+=lineWF; if(wFloat>=1){ w++; wFloat--; } //小数凑够1像素
h=stripesH[i];
//绘制上半部分
if(originY!=0){
y=originY-h-stripeHeight;
if(y<0){y=0;};
ctx.fillStyle=stripeLinear1;
ctx.fillRect(x, y, w, stripeHeight);
};
//绘制下半部分
if(originY!=height){
y=originY+h;
if(y+stripeHeight>height){
y=height-stripeHeight;
};
ctx.fillStyle=stripeLinear2;
ctx.fillRect(x, y, w, stripeHeight);
};
xFloat+=w;
};
};
if(iMirror){ ctx.restore(); }
if(!mirrorEnable) break;
};
}
drawBar() {
if (!this.animationStart) return
if (this.dataArray === null) return
analyser.getByteFrequencyData(this.dataArray)
this.opts.canvasCtx.fillStyle = this.opts.bgColor
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
let barWidth = (this.opts.WIDTH / this.bufferLength) * 1.2
let barHeight
let x = 0
let grd = this.opts.canvasCtx.createLinearGradient(0, 0, 0, this.opts.HEIGHT)
grd.addColorStop(0, '#0f0')
grd.addColorStop(0.5, '#ff0')
grd.addColorStop(0.5,"rgb(59, 234, 66)");
grd.addColorStop(0.5,"rgb(59, 234, 66)");
grd.addColorStop(1, '#f00')
for (var i = 0; i < this.bufferLength; i++) {
barHeight = this.dataArray[i] * this.opts.range
this.opts.canvasCtx.fillStyle = grd
this.opts.canvasCtx.fillRect(x, this.opts.HEIGHT - barHeight, barWidth, barHeight)
x += barWidth + 1
}
/* let color = this.opts.canvasCtx.createLinearGradient(
this.opts.WIDTH / 2,
this.opts.HEIGHT / 2 - 100,
this.opts.WIDTH / 2,
this.opts.HEIGHT / 2 + 100
);
color.addColorStop(0, "#32edf9");
color.addColorStop(0.5, "#e38cf5");
color.addColorStop(1, "#a2f591");
this.opts.canvasCtx.clearRect(0,0,this.opts.WIDTH,this.opts.HEIGHT);
let step = Math.round(this.dataArray.length / 200);
for (let i = 0; i < 200; i++) {
let j = this.dataArray[i * step];
// 设置填充色
this.opts.canvasCtx.fillStyle = color;
// 从中间向两边填充
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 + i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? j : j - 60);
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 - i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? j : j - 60);
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 + i * 10, this.opts.HEIGHT / 2, 5, j ==0 ? -j : -j + 60);
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 - i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? -j : -j + 60);
}*/
// window.requestAnimationFrame(debounce(this.drawBar, 60))
window.requestAnimationFrame(this.drawBar)
}
drawBar2() {
if (!this.animationStart) return
if (this.dataArray === null) return
analyser.getByteFrequencyData(this.dataArray)
this.opts.canvasCtx.fillStyle = this.opts.bgColor
let color = this.opts.canvasCtx.createLinearGradient(
this.opts.WIDTH / 2,
this.opts.HEIGHT / 2 - this.opts.HEIGHT / 2,
this.opts.WIDTH / 2,
this.opts.HEIGHT / 2 + this.opts.HEIGHT / 2
);
color.addColorStop(0, "#32edf9");
color.addColorStop(0.5, "#e38cf5");
color.addColorStop(1, "#a2f591");
this.opts.canvasCtx.clearRect(0,0,this.opts.WIDTH,this.opts.HEIGHT);
let step = Math.round(this.dataArray.length / 200);
for (let i = 0; i < 200; i++) {
let j = this.dataArray[i * step];
// 设置填充色
this.opts.canvasCtx.fillStyle = color;
// 从中间向两边填充
j = j / 255 * this.opts.HEIGHT / 2
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 + i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? j : j );
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 - i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? j : j );
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 + i * 10, this.opts.HEIGHT / 2, 5, j ==0 ? -j : -j );
this.opts.canvasCtx.fillRect(this.opts.WIDTH / 2 - i * 10, this.opts.HEIGHT / 2 , 5, j ==0 ? -j : -j );
}
// window.requestAnimationFrame(debounce(this.drawBar, 60))
window.requestAnimationFrame(this.drawBar2)
}
drawLine() {
if (!this.animationStart) return
if (this.dataArray === null) return
analyser.getByteTimeDomainData(this.dataArray)
this.opts.canvasCtx.fillStyle = 'rgb(0, 0, 0)'
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.opts.canvasCtx.lineWidth = 1
this.opts.canvasCtx.strokeStyle = 'rgb(59, 234, 46)'
this.opts.canvasCtx.beginPath()
let sliceWidth = this.opts.WIDTH * 1.0 / this.bufferLength
let x = 0
for (let i = 0; i < this.bufferLength; i++) {
let v = this.dataArray[i] / 128.0
let y = v * this.opts.HEIGHT / 3
if (i === 0) {
this.opts.canvasCtx.moveTo(x, y)
} else {
this.opts.canvasCtx.lineTo(x, y)
}
x += sliceWidth
}
this.opts.canvasCtx.lineTo(this.opts.WIDTH, this.opts.HEIGHT / 2)
this.opts.canvasCtx.stroke()
window.requestAnimationFrame(this.drawLine)
}
stopAnimation() {
this.animationStart = false
this.isOpen = false
this.clear()
}
startAnimation() {
this.isOpen = false
this.animationStart = true
}
clear() {
this.opts.canvasCtx.clearRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
}
drawLine2(peaks) {
let _this = this
let arr = peaks.map(function(item, i) { return item * 1000 })
this.opts.canvasCtx.fillStyle = this.opts.bgColor
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.opts.canvasCtx.lineWidth = 1
this.opts.canvasCtx.strokeStyle = 'rgb(59, 234, 46)'
this.opts.canvasCtx.beginPath()
let sliceWidth = this.opts.WIDTH * 1.0 / arr.length
let x = 0
let len = arr.length
this.tween.animate({
x: x,
duration: 2000,
sliceWidth,
count: len,
callback: function (i, x, sliceWidth) {
let y = arr[i] * _this.opts.range + (_this.opts.HEIGHT / 2)
if (i === 0) {
_this.opts.canvasCtx.moveTo(x, y)
} else {
_this.opts.canvasCtx.lineTo(x, y)
}
x += sliceWidth
_this.opts.canvasCtx.stroke()
return x
}
})
/*for (let i = 0; i < len; i++) {
let y = arr[i] * this.opts.range + (this.opts.HEIGHT / 2)
if (i === 0) {
this.opts.canvasCtx.moveTo(x, y)
} else {
this.opts.canvasCtx.lineTo(x, y)
}
x += sliceWidth
}*/
this.opts.canvasCtx.lineTo(this.opts.WIDTH, this.opts.HEIGHT / 2)
this.opts.canvasCtx.stroke()
}
/* drawLine2() {
let arr = this.analysisLine.mergedPeaks
this.opts.canvasCtx.fillStyle = this.opts.bgColor
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.opts.canvasCtx.lineWidth = 1
this.opts.canvasCtx.strokeStyle = 'rgb(59, 234, 46)'
this.params = {}
this.params.fillParent = true
this.width = 870
this.opts.canvasCtx.fillStyle = 'red'
this.drawLineToContext(this.opts.canvasCtx, arr)
this.opts.canvasCtx.closePath()
this.opts.canvasCtx.fill()
} */
drawLineToContext(ctx, peaks, absmax = 1, halfH = 64, offsetY = 0, start = 0, end = 870) {
if (!ctx) {
return
}
const length = peaks.length / 2
const scale = this.params.fillParent && this.width !== length ? this.width / length : 1
const first = Math.round(length * 0)
const last = Math.round(length * 1) + 1
if (first > end || last < start) {
return
}
const canvasStart = Math.min(first, start)
const canvasEnd = Math.max(last, end)
let i
let j
ctx.beginPath()
ctx.moveTo(
(canvasStart - first) * scale + 0.5,
halfH + offsetY
)
for (i = canvasStart; i < canvasEnd; i++) {
const peak = peaks[2 * i] || 0
const h = Math.round((peak / absmax) * halfH)
ctx.lineTo(
(i - first) * scale + 0.5,
halfH - h + offsetY
)
}
for (j = canvasEnd - 1; j >= canvasStart; j--) {
const peak = peaks[2 * j + 1] || 0
const h = Math.round((peak / absmax) * halfH)
ctx.lineTo(
(j - first) * scale + 0.5,
halfH - h + offsetY
)
}
}
drawLine3(buffer) {
let _this = this
let peaks = this.analysisLine.getPeaks(buffer)
if (_this.masterArr.length > (this.opts.WIDTH / 4 * 4)) {
let len = _this.masterArr.length - this.opts.WIDTH / 2
_this.masterArr.splice(len)
}
let arr = peaks.map(function(item, i) {
return item * 1000
})
_this.masterArr.unshift.apply(_this.masterArr, arr)
this.opts.canvasCtx.fillStyle = this.opts.bgColor
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.opts.canvasCtx.lineWidth = 1
this.opts.canvasCtx.strokeStyle = 'rgb(59, 234, 46)'
this.opts.canvasCtx.beginPath()
let sliceWidth = this.opts.WIDTH * 1.0 / _this.masterArr.length
let x = 0
let len = _this.masterArr.length
for (let i = 0; i < len; i++) {
let y = _this.masterArr[i] * this.opts.range + (this.opts.HEIGHT / 2)
if (i === 0) {
this.opts.canvasCtx.moveTo(x, y)
} else {
this.opts.canvasCtx.lineTo(x, y)
}
x += sliceWidth
}
this.opts.canvasCtx.lineTo(this.opts.WIDTH, this.opts.HEIGHT / 2)
this.opts.canvasCtx.stroke()
}
/*drawLine3() {
let _this = this
if (_this.masterArr.length > (this.opts.WIDTH / 2 * 4)) {
let len = _this.masterArr.length - this.opts.WIDTH / 2
_this.masterArr.splice(len)
}
let arr = this.analysisLine.mergedPeaks
_this.masterArr.unshift.apply(_this.masterArr, arr)
this.opts.canvasCtx.fillStyle = this.opts.bgColor
this.opts.canvasCtx.fillRect(0, 0, this.opts.WIDTH, this.opts.HEIGHT)
this.params = {}
this.params.fillParent = true
this.width = 870
this.opts.canvasCtx.fillStyle = 'rgb(59, 234, 46)'
this.drawLineToContext(this.opts.canvasCtx, _this.masterArr)
this.opts.canvasCtx.closePath()
this.opts.canvasCtx.fill()
}*/
}
// 音谱条对象
function Retangle(w, h, x, y, octx, grd) {
this.grd = grd
this.octx = octx
this.w = w
this.h = h // 小红块高度
this.x = x
this.y = y
this.jg = 3
this.power = 0
this.dy = y // 小红块位置
this.num = 0
}
var Rp = Retangle.prototype
Rp.update = function(power) {
this.power = power
this.num = ~~(this.power / this.h + 0.5)
//更新小红块的位置,如果音频条长度高于红块位置,则红块位置则为音频条高度,否则让小红块下降
var nh = this.dy + this.h//小红块当前位置
if (this.power >= this.y - nh) {
this.dy = this.y - this.power - this.h - (this.power === 0 ? 0 : 1)
} else if (nh > this.y) {
this.dy = this.y - this.h
} else {
this.dy += 1
}
this.draw()
}
Rp.draw = function() {
this.octx.fillStyle = this.grd
var h = (~~(this.power / (this.h + this.jg))) * (this.h + this.jg)
this.octx.fillRect(this.x, this.y - h, this.w, h)
for (var i = 0; i < this.num; i++) {
var y = this.y - i * (this.h + this.jg)
this.octx.clearRect(this.x - 1, y, this.w + 2, this.jg)
}
//octx.fillStyle = "#950000";
//octx.fillRect(this.x, ~~this.dy, this.w, this.h);
}