Spaces:
Runtime error
Runtime error
File size: 11,510 Bytes
d59aeff |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
/*
录音 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(){
"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;
})(); |