generated from template/vite-react-template
377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
/*
|
||
录音 Recorder扩展,频率直方图显示
|
||
使用本扩展需要引入src/extensions/lib.fft.js支持,直方图特意优化主要显示0-5khz语音部分(线性),其他高频显示区域较小,不适合用来展示音乐频谱,可通过配置fullFreq来恢复成完整的线性频谱,或自行修改源码修改成倍频程频谱(伯德图、对数频谱);本可视化插件可以移植到其他语言环境,如需定制可联系作者
|
||
|
||
https://github.com/xiangyuecn/Recorder
|
||
|
||
本扩展核心算法主要参考了Java开源库jmp123 版本0.3 的代码:
|
||
https://www.iteye.com/topic/851459
|
||
https://sourceforge.net/projects/jmp123/files/
|
||
*/
|
||
(function(factory){
|
||
var browser=typeof window=="object" && !!window.document;
|
||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||
var rec=win.Recorder,ni=rec.i18n;
|
||
factory(rec,ni,ni.$T,browser);
|
||
}(function(Recorder,i18n,$T,isBrowser){
|
||
"use strict";
|
||
|
||
var FrequencyHistogramView=function(set){
|
||
return new fn(set);
|
||
};
|
||
var ViewTxt="FrequencyHistogramView";
|
||
var fn=function(set){
|
||
var This=this;
|
||
var o={
|
||
/*
|
||
elem:"css selector" //自动显示到dom,并以此dom大小为显示大小
|
||
//或者配置显示大小,手动把frequencyObj.elem显示到别的地方
|
||
,width:0 //显示宽度
|
||
,height:0 //显示高度
|
||
|
||
H5环境以上配置二选一
|
||
|
||
compatibleCanvas: CanvasObject //提供一个兼容H5的canvas对象,需支持getContext("2d"),支持设置width、height,支持drawImage(canvas,...)
|
||
,width:0 //canvas显示宽度
|
||
,height:0 //canvas显示高度
|
||
非H5环境使用以上配置
|
||
*/
|
||
|
||
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输入和计算时间
|
||
,onDraw:function(frequencyData,sampleRate){}
|
||
};
|
||
for(var k in set){
|
||
o[k]=set[k];
|
||
};
|
||
This.set=set=o;
|
||
|
||
var cCanvas="compatibleCanvas";
|
||
if(set[cCanvas]){
|
||
var canvas=This.canvas=set[cCanvas];
|
||
}else{
|
||
if(!isBrowser)throw new Error($T.G("NonBrowser-1",[ViewTxt]));
|
||
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 thisElem=This.elem=document.createElement("div");
|
||
thisElem.style.fontSize=0;
|
||
thisElem.innerHTML='<canvas style="width:100%;height:100%;"/>';
|
||
|
||
var canvas=This.canvas=thisElem.querySelector("canvas");
|
||
|
||
if(elem){
|
||
elem.innerHTML="";
|
||
elem.appendChild(thisElem);
|
||
};
|
||
};
|
||
var scale=set.scale;
|
||
var width=set.width*scale;
|
||
var height=set.height*scale;
|
||
if(!width || !height){
|
||
throw new Error($T.G("IllegalArgs-1",[ViewTxt+" width=0 height=0"]));
|
||
};
|
||
|
||
canvas.width=width;
|
||
canvas.height=height;
|
||
var ctx=This.ctx=canvas.getContext("2d");
|
||
|
||
if(!Recorder.LibFFT){
|
||
throw new Error($T.G("NeedImport-2",[ViewTxt,"src/extensions/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;
|
||
|
||
This.lastH=[];//重置高度再绘制一次,避免定时不准没到底就停了
|
||
This.stripesH=[];
|
||
This.draw(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.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,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;
|
||
};
|
||
|
||
if(frequencyData){
|
||
set.onDraw(frequencyData,sampleRate);
|
||
};
|
||
}
|
||
};
|
||
Recorder[ViewTxt]=FrequencyHistogramView;
|
||
|
||
|
||
})); |