mirror of
https://github.com/kuaifan/dootask.git
synced 2026-01-15 19:28:11 +00:00
perf: 优化录音、优化会议
This commit is contained in:
parent
1ca397d19b
commit
1ac069aba4
30
public/js/AgoraRTC_N-4.14.2.js
vendored
Normal file
30
public/js/AgoraRTC_N-4.14.2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
338
public/js/recorder/frequency.histogram.view.js
vendored
338
public/js/recorder/frequency.histogram.view.js
vendored
@ -1,338 +1,6 @@
|
||||
/*
|
||||
录音 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/
|
||||
src: extensions/frequency.histogram.view.js
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
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;
|
||||
|
||||
|
||||
})();
|
||||
!function(){"use strict";var t=function(t){return new e(t)},d="FrequencyHistogramView",e=function(t){var e=this,r={scale:2,fps:20,lineCount:30,widthRatio:.6,spaceWidth:0,minHeight:0,position:-1,mirrorEnable:!1,stripeEnable:!0,stripeHeight:3,stripeMargin:6,fallDuration:1e3,stripeFallDuration:3500,linear:[0,"rgba(0,187,17,1)",.5,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"],stripeLinear:null,shadowBlur:0,shadowColor:"#bbb",stripeShadowBlur:-1,stripeShadowColor:"",onDraw:function(t,e){}};for(var a in t)r[a]=t[a];e.set=t=r;var i=t.elem;i&&("string"==typeof i?i=document.querySelector(i):i.length&&(i=i[0])),i&&(t.width=i.offsetWidth,t.height=i.offsetHeight);var o=t.scale,n=t.width*o,l=t.height*o;if(!n||!l)throw new Error(d+"无宽高");var h=e.elem=document.createElement("div"),s=["","transform-origin:0 0;","transform:scale("+1/o+");"];h.innerHTML='<div style="width:'+t.width+"px;height:"+t.height+'px;overflow:hidden"><div style="width:'+n+"px;height:"+l+"px;"+s.join("-webkit-")+s.join("-ms-")+s.join("-moz-")+s.join("")+'"><canvas/></div></div>';var f=e.canvas=h.querySelector("canvas");e.ctx=f.getContext("2d");if(f.width=n,f.height=l,i&&(i.innerHTML="",i.appendChild(h)),!Recorder.LibFFT)throw new Error("需要lib.fft.js支持");e.fft=Recorder.LibFFT(1024),e.lastH=[],e.stripesH=[]};e.prototype=t.prototype={genLinear:function(t,e,r,a){for(var i=t.createLinearGradient(0,r,0,a),o=0;o<e.length;)i.addColorStop(e[o++],e[o++]);return i},input:function(t,e,r){var a=this;a.sampleRate=r,a.pcmData=t,a.pcmPos=0,a.inputTime=Date.now(),a.schedule()},schedule:function(){var t=this,e=t.set,r=Math.floor(1e3/e.fps);t.timer||(t.timer=setInterval(function(){t.schedule()},r));var a=Date.now(),i=t.drawTime||0;if(a-t.inputTime>1.3*e.stripeFallDuration)return clearInterval(t.timer),void(t.timer=0);if(!(a-i<r)){t.drawTime=a;for(var o=t.fft.bufferSize,n=t.pcmData,l=t.pcmPos,h=new Int16Array(o),s=0;s<o&&l<n.length;s++,l++)h[s]=n[l];t.pcmPos=l;var f=t.fft.transform(h);t.draw(f,t.sampleRate)}},draw:function(t,e){var r=this,a=r.set,i=r.ctx,o=a.scale,n=a.width*o,l=a.height*o,h=a.lineCount,s=r.fft.bufferSize,f=a.position,d=Math.abs(a.position),c=1==f?0:l,p=l;d<1&&(c=p/=2,p=Math.floor(p*(1+d)),c=Math.floor(0<f?c*(1-d):c*(1+d)));for(var u=r.lastH,w=r.stripesH,v=Math.ceil(p/(a.fallDuration/(1e3/a.fps))),g=Math.ceil(p/(a.stripeFallDuration/(1e3/a.fps))),m=a.stripeMargin*o,M=1<<(Math.round(Math.log(s)/Math.log(2)+3)<<1),b=Math.log(M)/Math.log(10),L=20*Math.log(32767)/Math.log(10),y=s/2,S=Math.min(y,Math.floor(5e3*y/(e/2))),C=S==y,H=C?h:Math.round(.8*h),R=S/H,D=C?0:(y-S)/(h-H),x=0,F=0;F<h;F++){var T=Math.ceil(x);x+=F<H?R:D;for(var B=Math.min(Math.ceil(x),y),E=0,j=T;j<B;j++)E=Math.max(E,Math.abs(t[j]));var I=M<E?Math.floor(17*(Math.log(E)/Math.log(10)-b)):0,q=p*Math.min(I/L,1);u[F]=(u[F]||0)-v,q<u[F]&&(q=u[F]),q<0&&(q=0),u[F]=q;var z=w[F]||0;if(q&&z<q+m)w[F]=q+m;else{var P=z-g;P<0&&(P=0),w[F]=P}}i.clearRect(0,0,n,l);var W=r.genLinear(i,a.linear,c,c-p),k=a.stripeLinear&&r.genLinear(i,a.stripeLinear,c,c-p)||W,A=r.genLinear(i,a.linear,c,c+p),G=a.stripeLinear&&r.genLinear(i,a.stripeLinear,c,c+p)||A;i.shadowBlur=a.shadowBlur*o,i.shadowColor=a.shadowColor;var V=a.mirrorEnable,J=V?2*h-1:h,K=a.widthRatio,N=a.spaceWidth*o;0!=N&&(K=(n-N*(J+1))/n);for(var O=Math.max(1*o,Math.floor(n*K/J)),Q=(n-J*O)/(J+1),U=a.minHeight*o,X=V?n/2-(Q+O/2):0,Y=(F=0,X);F<h;F++)Y+=Q,$=Math.floor(Y),q=Math.max(u[F],U),0!=c&&(_=c-q,i.fillStyle=W,i.fillRect($,_,O,q)),c!=l&&(i.fillStyle=A,i.fillRect($,c,O,q)),Y+=O;if(a.stripeEnable){var Z=a.stripeShadowBlur;i.shadowBlur=(-1==Z?a.shadowBlur:Z)*o,i.shadowColor=a.stripeShadowColor||a.shadowColor;var $,_,tt=a.stripeHeight*o;for(F=0,Y=X;F<h;F++)Y+=Q,$=Math.floor(Y),q=w[F],0!=c&&((_=c-q-tt)<0&&(_=0),i.fillStyle=k,i.fillRect($,_,O,tt)),c!=l&&(l<(_=c+q)+tt&&(_=l-tt),i.fillStyle=G,i.fillRect($,_,O,tt)),Y+=O}if(V){var et=Math.floor(n/2);i.save(),i.scale(-1,1),i.drawImage(r.canvas,Math.ceil(n/2),0,et,l,-et,0,et,l),i.restore()}a.onDraw(t,e)}},Recorder[d]=t}();
|
||||
111
public/js/recorder/lib.fft.js
vendored
111
public/js/recorder/lib.fft.js
vendored
@ -1,111 +1,6 @@
|
||||
/*
|
||||
时域转频域,快速傅里叶变换(FFT)
|
||||
录音
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
var fft=Recorder.LibFFT(bufferSize)
|
||||
bufferSize取值2的n次方
|
||||
|
||||
fft.bufferSize 实际采用的bufferSize
|
||||
fft.transform(inBuffer)
|
||||
inBuffer:[Int16,...] 数组长度必须是bufferSize
|
||||
返回[Float64(Long),...],长度为bufferSize/2
|
||||
src: extensions/lib.fft.js
|
||||
*/
|
||||
|
||||
/*
|
||||
从FFT.java 移植,Java开源库:jmp123 版本0.3
|
||||
https://www.iteye.com/topic/851459
|
||||
https://sourceforge.net/projects/jmp123/files/
|
||||
*/
|
||||
Recorder.LibFFT=function(bufferSize){
|
||||
"use strict";
|
||||
|
||||
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};
|
||||
};
|
||||
Recorder.LibFFT=function(r){"use strict";var s,v,d,l,F,b,g,m;return function(r){var o,t,a,f;for(s=Math.round(Math.log(r)/Math.log(2)),d=((v=1<<s)<<2)*Math.sqrt(2),l=[],F=[],b=[0],g=[0],m=[],o=0;o<v;o++){for(a=o,f=t=0;t!=s;t++)f<<=1,f|=1&a,a>>>=1;m[o]=f}var n,u=2*Math.PI/v;for(o=(v>>1)-1;0<o;o--)n=o*u,g[o]=Math.cos(n),b[o]=Math.sin(n)}(r),{transform:function(r){var o,t,a,f,n,u,e,h,M=1,i=s-1;for(o=0;o!=v;o++)l[o]=r[m[o]],F[o]=0;for(o=s;0!=o;o--){for(t=0;t!=M;t++)for(n=g[t<<i],u=b[t<<i],a=t;a<v;a+=M<<1)e=n*l[f=a+M]-u*F[f],h=n*F[f]+u*l[f],l[f]=l[a]-e,F[f]=F[a]-h,l[a]+=e,F[a]+=h;M<<=1,i--}t=v>>1;var c=new Float64Array(t);for(n=-(u=d),o=t;0!=o;o--)e=l[o],h=F[o],c[o-1]=n<e&&e<u&&n<h&&h<u?0:Math.round(e*e+h*h);return c},bufferSize:v}};
|
||||
2
public/js/recorder/recorder.mp3.min.js
vendored
2
public/js/recorder/recorder.mp3.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
30
resources/assets/statics/public/js/AgoraRTC_N-4.14.2.js
vendored
Normal file
30
resources/assets/statics/public/js/AgoraRTC_N-4.14.2.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,338 +1,6 @@
|
||||
/*
|
||||
录音 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/
|
||||
src: extensions/frequency.histogram.view.js
|
||||
*/
|
||||
(function(){
|
||||
"use strict";
|
||||
|
||||
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;
|
||||
|
||||
|
||||
})();
|
||||
!function(){"use strict";var t=function(t){return new e(t)},d="FrequencyHistogramView",e=function(t){var e=this,r={scale:2,fps:20,lineCount:30,widthRatio:.6,spaceWidth:0,minHeight:0,position:-1,mirrorEnable:!1,stripeEnable:!0,stripeHeight:3,stripeMargin:6,fallDuration:1e3,stripeFallDuration:3500,linear:[0,"rgba(0,187,17,1)",.5,"rgba(255,215,0,1)",1,"rgba(255,102,0,1)"],stripeLinear:null,shadowBlur:0,shadowColor:"#bbb",stripeShadowBlur:-1,stripeShadowColor:"",onDraw:function(t,e){}};for(var a in t)r[a]=t[a];e.set=t=r;var i=t.elem;i&&("string"==typeof i?i=document.querySelector(i):i.length&&(i=i[0])),i&&(t.width=i.offsetWidth,t.height=i.offsetHeight);var o=t.scale,n=t.width*o,l=t.height*o;if(!n||!l)throw new Error(d+"无宽高");var h=e.elem=document.createElement("div"),s=["","transform-origin:0 0;","transform:scale("+1/o+");"];h.innerHTML='<div style="width:'+t.width+"px;height:"+t.height+'px;overflow:hidden"><div style="width:'+n+"px;height:"+l+"px;"+s.join("-webkit-")+s.join("-ms-")+s.join("-moz-")+s.join("")+'"><canvas/></div></div>';var f=e.canvas=h.querySelector("canvas");e.ctx=f.getContext("2d");if(f.width=n,f.height=l,i&&(i.innerHTML="",i.appendChild(h)),!Recorder.LibFFT)throw new Error("需要lib.fft.js支持");e.fft=Recorder.LibFFT(1024),e.lastH=[],e.stripesH=[]};e.prototype=t.prototype={genLinear:function(t,e,r,a){for(var i=t.createLinearGradient(0,r,0,a),o=0;o<e.length;)i.addColorStop(e[o++],e[o++]);return i},input:function(t,e,r){var a=this;a.sampleRate=r,a.pcmData=t,a.pcmPos=0,a.inputTime=Date.now(),a.schedule()},schedule:function(){var t=this,e=t.set,r=Math.floor(1e3/e.fps);t.timer||(t.timer=setInterval(function(){t.schedule()},r));var a=Date.now(),i=t.drawTime||0;if(a-t.inputTime>1.3*e.stripeFallDuration)return clearInterval(t.timer),void(t.timer=0);if(!(a-i<r)){t.drawTime=a;for(var o=t.fft.bufferSize,n=t.pcmData,l=t.pcmPos,h=new Int16Array(o),s=0;s<o&&l<n.length;s++,l++)h[s]=n[l];t.pcmPos=l;var f=t.fft.transform(h);t.draw(f,t.sampleRate)}},draw:function(t,e){var r=this,a=r.set,i=r.ctx,o=a.scale,n=a.width*o,l=a.height*o,h=a.lineCount,s=r.fft.bufferSize,f=a.position,d=Math.abs(a.position),c=1==f?0:l,p=l;d<1&&(c=p/=2,p=Math.floor(p*(1+d)),c=Math.floor(0<f?c*(1-d):c*(1+d)));for(var u=r.lastH,w=r.stripesH,v=Math.ceil(p/(a.fallDuration/(1e3/a.fps))),g=Math.ceil(p/(a.stripeFallDuration/(1e3/a.fps))),m=a.stripeMargin*o,M=1<<(Math.round(Math.log(s)/Math.log(2)+3)<<1),b=Math.log(M)/Math.log(10),L=20*Math.log(32767)/Math.log(10),y=s/2,S=Math.min(y,Math.floor(5e3*y/(e/2))),C=S==y,H=C?h:Math.round(.8*h),R=S/H,D=C?0:(y-S)/(h-H),x=0,F=0;F<h;F++){var T=Math.ceil(x);x+=F<H?R:D;for(var B=Math.min(Math.ceil(x),y),E=0,j=T;j<B;j++)E=Math.max(E,Math.abs(t[j]));var I=M<E?Math.floor(17*(Math.log(E)/Math.log(10)-b)):0,q=p*Math.min(I/L,1);u[F]=(u[F]||0)-v,q<u[F]&&(q=u[F]),q<0&&(q=0),u[F]=q;var z=w[F]||0;if(q&&z<q+m)w[F]=q+m;else{var P=z-g;P<0&&(P=0),w[F]=P}}i.clearRect(0,0,n,l);var W=r.genLinear(i,a.linear,c,c-p),k=a.stripeLinear&&r.genLinear(i,a.stripeLinear,c,c-p)||W,A=r.genLinear(i,a.linear,c,c+p),G=a.stripeLinear&&r.genLinear(i,a.stripeLinear,c,c+p)||A;i.shadowBlur=a.shadowBlur*o,i.shadowColor=a.shadowColor;var V=a.mirrorEnable,J=V?2*h-1:h,K=a.widthRatio,N=a.spaceWidth*o;0!=N&&(K=(n-N*(J+1))/n);for(var O=Math.max(1*o,Math.floor(n*K/J)),Q=(n-J*O)/(J+1),U=a.minHeight*o,X=V?n/2-(Q+O/2):0,Y=(F=0,X);F<h;F++)Y+=Q,$=Math.floor(Y),q=Math.max(u[F],U),0!=c&&(_=c-q,i.fillStyle=W,i.fillRect($,_,O,q)),c!=l&&(i.fillStyle=A,i.fillRect($,c,O,q)),Y+=O;if(a.stripeEnable){var Z=a.stripeShadowBlur;i.shadowBlur=(-1==Z?a.shadowBlur:Z)*o,i.shadowColor=a.stripeShadowColor||a.shadowColor;var $,_,tt=a.stripeHeight*o;for(F=0,Y=X;F<h;F++)Y+=Q,$=Math.floor(Y),q=w[F],0!=c&&((_=c-q-tt)<0&&(_=0),i.fillStyle=k,i.fillRect($,_,O,tt)),c!=l&&(l<(_=c+q)+tt&&(_=l-tt),i.fillStyle=G,i.fillRect($,_,O,tt)),Y+=O}if(V){var et=Math.floor(n/2);i.save(),i.scale(-1,1),i.drawImage(r.canvas,Math.ceil(n/2),0,et,l,-et,0,et,l),i.restore()}a.onDraw(t,e)}},Recorder[d]=t}();
|
||||
@ -1,111 +1,6 @@
|
||||
/*
|
||||
时域转频域,快速傅里叶变换(FFT)
|
||||
录音
|
||||
https://github.com/xiangyuecn/Recorder
|
||||
|
||||
var fft=Recorder.LibFFT(bufferSize)
|
||||
bufferSize取值2的n次方
|
||||
|
||||
fft.bufferSize 实际采用的bufferSize
|
||||
fft.transform(inBuffer)
|
||||
inBuffer:[Int16,...] 数组长度必须是bufferSize
|
||||
返回[Float64(Long),...],长度为bufferSize/2
|
||||
src: extensions/lib.fft.js
|
||||
*/
|
||||
|
||||
/*
|
||||
从FFT.java 移植,Java开源库:jmp123 版本0.3
|
||||
https://www.iteye.com/topic/851459
|
||||
https://sourceforge.net/projects/jmp123/files/
|
||||
*/
|
||||
Recorder.LibFFT=function(bufferSize){
|
||||
"use strict";
|
||||
|
||||
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};
|
||||
};
|
||||
Recorder.LibFFT=function(r){"use strict";var s,v,d,l,F,b,g,m;return function(r){var o,t,a,f;for(s=Math.round(Math.log(r)/Math.log(2)),d=((v=1<<s)<<2)*Math.sqrt(2),l=[],F=[],b=[0],g=[0],m=[],o=0;o<v;o++){for(a=o,f=t=0;t!=s;t++)f<<=1,f|=1&a,a>>>=1;m[o]=f}var n,u=2*Math.PI/v;for(o=(v>>1)-1;0<o;o--)n=o*u,g[o]=Math.cos(n),b[o]=Math.sin(n)}(r),{transform:function(r){var o,t,a,f,n,u,e,h,M=1,i=s-1;for(o=0;o!=v;o++)l[o]=r[m[o]],F[o]=0;for(o=s;0!=o;o--){for(t=0;t!=M;t++)for(n=g[t<<i],u=b[t<<i],a=t;a<v;a+=M<<1)e=n*l[f=a+M]-u*F[f],h=n*F[f]+u*l[f],l[f]=l[a]-e,F[f]=F[a]-h,l[a]+=e,F[a]+=h;M<<=1,i--}t=v>>1;var c=new Float64Array(t);for(n=-(u=d),o=t;0!=o;o--)e=l[o],h=F[o],c[o-1]=n<e&&e<u&&n<h&&h<u?0:Math.round(e*e+h*h);return c},bufferSize:v}};
|
||||
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user