Spaces:
Runtime error
Runtime error
/* | |
录音 Recorder扩展,频率直方图显示 | |
使用本扩展需要引入lib.fft.js支持,直方图特意优化主要显示0-5khz语音部分,其他高频显示区域较小,不适合用来展示音乐频谱 | |
https://github.com/xiangyuecn/Recorder | |
本扩展核心算法主要参考了Java开源库jmp123 版本0.3 的代码: | |
https://www.iteye.com/topic/851459 | |
https://sourceforge.net/projects/jmp123/files/ | |
*/ | |
(function(){ | |
; | |
var FrequencyHistogramView=function(set){ | |
return new fn(set); | |
}; | |
var fn=function(set){ | |
var This=this; | |
var o={ | |
/* | |
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小 | |
//或者配置显示大小,手动把frequencyObj.elem显示到别的地方 | |
,width:0 //显示宽度 | |
,height:0 //显示高度 | |
以上配置二选一 | |
*/ | |
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:"" //峰值小横条阴影颜色,留空为柱子的阴影颜色 | |
//当发生绘制时会回调此方法,参数为当前绘制的频率数据和采样率,可实现多个直方图同时绘制,只消耗一个input输入和计算时间 | |
,onDraw:function(frequencyData,sampleRate){} | |
}; | |
for(var k in set){ | |
o[k]=set[k]; | |
}; | |
This.set=set=o; | |
var elem=set.elem; | |
if(elem){ | |
if(typeof(elem)=="string"){ | |
elem=document.querySelector(elem); | |
}else if(elem.length){ | |
elem=elem[0]; | |
}; | |
}; | |
if(elem){ | |
set.width=elem.offsetWidth; | |
set.height=elem.offsetHeight; | |
}; | |
var scale=set.scale; | |
var width=set.width*scale; | |
var height=set.height*scale; | |
var thisElem=This.elem=document.createElement("div"); | |
var lowerCss=["","transform-origin:0 0;","transform:scale("+(1/scale)+");"]; | |
thisElem.innerHTML='<div style="width:'+set.width+'px;height:'+set.height+'px;overflow:hidden"><div style="width:'+width+'px;height:'+height+'px;'+lowerCss.join("-webkit-")+lowerCss.join("-ms-")+lowerCss.join("-moz-")+lowerCss.join("")+'"><canvas/></div></div>'; | |
var canvas=This.canvas=thisElem.querySelector("canvas"); | |
var ctx=This.ctx=canvas.getContext("2d"); | |
canvas.width=width; | |
canvas.height=height; | |
if(elem){ | |
elem.innerHTML=""; | |
elem.appendChild(thisElem); | |
}; | |
if(!Recorder.LibFFT){ | |
throw new Error("需要lib.fft.js支持"); | |
}; | |
This.fft=Recorder.LibFFT(1024); | |
//柱子所在高度 | |
This.lastH=[]; | |
//峰值小横条所在高度 | |
This.stripesH=[]; | |
}; | |
fn.prototype=FrequencyHistogramView.prototype={ | |
genLinear:function(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:function(pcmData,powerLevel,sampleRate){ | |
var This=this; | |
This.sampleRate=sampleRate; | |
This.pcmData=pcmData; | |
This.pcmPos=0; | |
This.inputTime=Date.now(); | |
This.schedule(); | |
} | |
,schedule:function(){ | |
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; | |
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.draw(frequencyData,This.sampleRate); | |
} | |
,draw:function(frequencyData,sampleRate){ | |
var This=this,set=This.set; | |
var ctx=This.ctx; | |
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; | |
var fftSize5k=Math.min(fftSize,Math.floor(fftSize*5000/(sampleRate/2)));//5khz所在位置,8000采样率及以下最高只有4khz | |
var fftSize5kIsAll=fftSize5k==fftSize; | |
var line80=fftSize5kIsAll?lineCount:Math.round(lineCount*0.8);//80%的柱子位置 | |
var fftSizeStep1=fftSize5k/line80; | |
var fftSizeStep2=fftSize5kIsAll?0:(fftSize-fftSize5k)/(lineCount-line80); | |
var fftIdx=0; | |
for(var i=0;i<lineCount;i++){ | |
//不采用jmp123的非线性划分频段,录音语音并不适用于音乐的频率,应当弱化高频部分 | |
//80%关注0-5khz主要人声部分 20%关注剩下的高频,这样不管什么采样率都能做到大部分频率显示一致。 | |
var start=Math.ceil(fftIdx); | |
if(i<line80){ | |
//5khz以下 | |
fftIdx+=fftSizeStep1; | |
}else{ | |
//5khz以上 | |
fftIdx+=fftSizeStep2; | |
}; | |
var end=Math.min(Math.ceil(fftIdx),fftSize); | |
//参考AudioGUI.java .drawHistogram方法 | |
//查找当前频段的最大"幅值" | |
var maxAmp=0; | |
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;//上半部分的峰值小横条填充 | |
//计算柱子间距 | |
ctx.shadowBlur=set.shadowBlur*scale; | |
ctx.shadowColor=set.shadowColor; | |
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; | |
}; | |
var lineWidth=Math.max(1*scale,Math.floor((width*widthRatio)/mirrorCount));//柱子宽度至少1个单位 | |
var spaceFloat=(width-mirrorCount*lineWidth)/(mirrorCount+1);//均匀间隔,首尾都留空,可能为负数,柱子将发生重叠 | |
//绘制柱子 | |
var minHeight=set.minHeight*scale; | |
var mirrorSubX=spaceFloat+lineWidth/2; | |
var XFloat=mirrorEnable?width/2-mirrorSubX:0;//镜像时,中间柱子位于正中心 | |
for(var i=0,xFloat=XFloat,x,y,h;i<lineCount;i++){ | |
xFloat+=spaceFloat; | |
x=Math.floor(xFloat); | |
h=Math.max(lastH[i],minHeight); | |
//绘制上半部分 | |
if(originY!=0){ | |
y=originY-h; | |
ctx.fillStyle=linear1; | |
ctx.fillRect(x, y, lineWidth, h); | |
}; | |
//绘制下半部分 | |
if(originY!=height){ | |
ctx.fillStyle=linear2; | |
ctx.fillRect(x, originY, lineWidth, h); | |
}; | |
xFloat+=lineWidth; | |
}; | |
//绘制柱子顶上峰值小横条 | |
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,x,y,h;i<lineCount;i++){ | |
xFloat+=spaceFloat; | |
x=Math.floor(xFloat); | |
h=stripesH[i]; | |
//绘制上半部分 | |
if(originY!=0){ | |
y=originY-h-stripeHeight; | |
if(y<0){y=0;}; | |
ctx.fillStyle=stripeLinear1; | |
ctx.fillRect(x, y, lineWidth, stripeHeight); | |
}; | |
//绘制下半部分 | |
if(originY!=height){ | |
y=originY+h; | |
if(y+stripeHeight>height){ | |
y=height-stripeHeight; | |
}; | |
ctx.fillStyle=stripeLinear2; | |
ctx.fillRect(x, y, lineWidth, stripeHeight); | |
}; | |
xFloat+=lineWidth; | |
}; | |
}; | |
//镜像,从中间直接镜像即可 | |
if(mirrorEnable){ | |
var srcW=Math.floor(width/2); | |
ctx.save(); | |
ctx.scale(-1,1); | |
ctx.drawImage(This.canvas,Math.ceil(width/2),0,srcW,height,-srcW,0,srcW,height); | |
ctx.restore(); | |
}; | |
set.onDraw(frequencyData,sampleRate); | |
} | |
}; | |
Recorder.FrequencyHistogramView=FrequencyHistogramView; | |
})(); |