UNPKG

vue-waveform

Version:

waveform audio player wavesurfer -waveform js html 音频audio波形图

947 lines (845 loc) 33.3 kB
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); }