UNPKG

recorder-core

Version:

Recorder库: html5 js 录音 mp3 wav ogg webm amr g711a g711u 格式,支持pc和Android、iOS部分浏览器、Hybrid App(提供Android iOS App源码)、微信,提供ASR语音识别转文字 H5版语音通话聊天示例 DTMF编码解码

533 lines (475 loc) 15.9 kB
/* mp3编码器,需带上src/engine/mp3-engine.js引擎使用 https://github.com/xiangyuecn/Recorder 当然最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式 浏览器支持情况 https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats */ (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 SampleS="48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000"; var BitS="8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320"; Recorder.prototype.enc_mp3={ stable:true,takeEC:"full" ,getTestMsg:function(){ return $T("Zm7L::采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)",0,SampleS,BitS); } }; var NormalizeSet=function(set){ var bS=set.bitRate, sS=set.sampleRate,s=sS; if((" "+BitS+",").indexOf(" "+bS+",")==-1){ Recorder.CLog($T("eGB9::{1}不在mp3支持的取值范围:{2}",0,"bitRate="+bS,BitS),3); } if((" "+SampleS+",").indexOf(" "+sS+",")==-1){//engine SmpFrqIndex函数会检测 var arr=SampleS.split(", "),vs=[]; for(var i=0;i<arr.length;i++) vs.push({v:+arr[i],s:Math.abs(arr[i]-sS)}); vs.sort(function(a,b){return a.s-b.s}); s=vs[0].v; set.sampleRate=s; Recorder.CLog($T("zLTa::sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}",0,s,sS,SampleS),3); } }; var ImportEngineErr=function(){ return $T.G("NeedImport-2",["mp3.js","src/engine/mp3-engine.js"]); }; //是否支持web worker var HasWebWorker=isBrowser && typeof Worker=="function"; //*******标准UI线程转码支持函数************ Recorder.prototype.mp3=function(res,True,False){ var This=this,set=This.set,size=res.length; if(!Recorder.lamejs){ False(ImportEngineErr()); return; }; //优先采用worker编码,非worker时用老方法提供兼容 if(HasWebWorker){ var ctx=This.mp3_start(set); if(ctx){ if(ctx.isW){ This.mp3_encode(ctx,res); This.mp3_complete(ctx,True,False,1); return; } This.mp3_stop(ctx); }; }; NormalizeSet(set); //https://github.com/wangpengfei15975/recorder.js //https://github.com/zhuker/lamejs bug:采样率必须和源一致,不然8k时没有声音,有问题fix:https://github.com/zhuker/lamejs/pull/11 var mp3=new Recorder.lamejs.Mp3Encoder(1,set.sampleRate,set.bitRate); var blockSize=57600; var memory=new Int8Array(500000), mOffset=0; var idx=0,isFlush=0; var run=function(){ try{ if(idx<size){ var buf=mp3.encodeBuffer(res.subarray(idx,idx+blockSize)); }else{ isFlush=1; var buf=mp3.flush(); }; }catch(e){ //精简代码调用了abort console.error(e); if(!isFlush) try{ mp3.flush() }catch(r){ console.error(r) } False("MP3 Encoder: "+e.message); return; }; var bufLen=buf.length; if(bufLen>0){ if(mOffset+bufLen>memory.length){ var tmp=new Int8Array(memory.length+Math.max(500000,bufLen)); tmp.set(memory.subarray(0, mOffset)); memory=tmp; } memory.set(buf,mOffset); mOffset+=bufLen; }; if(idx<size){ idx+=blockSize; setTimeout(run);//尽量避免卡ui }else{ var data=[memory.buffer.slice(0,mOffset)]; //去掉开头的标记信息帧 var meta=mp3TrimFix.fn(data,mOffset,size,set.sampleRate); mp3TrimFixSetMeta(meta,set); True(data[0]||new ArrayBuffer(0),"audio/mp3"); }; }; run(); } //********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码********* //全局共享一个Worker,后台串行执行。如果每次都开一个新的,编码速度可能会慢很多,可能是浏览器运行缓存的因素,并且可能瞬间产生多个并行操作占用大量cpu var mp3Worker; Recorder.BindDestroy("mp3Worker",function(){ if(mp3Worker){ Recorder.CLog("mp3Worker Destroy"); mp3Worker.terminate(); mp3Worker=null; }; }); Recorder.prototype.mp3_envCheck=function(envInfo,set){//检查环境下配置是否可用 var errMsg=""; //需要实时编码返回数据,此时需要检查是否可实时编码 if(set.takeoffEncodeChunk){ if(!newContext()){//浏览器不能创建实时编码环境 errMsg=$T("yhUs::当前浏览器版本太低,无法实时处理"); }; }; if(!errMsg && !Recorder.lamejs){ errMsg=ImportEngineErr(); }; return errMsg; }; Recorder.prototype.mp3_start=function(set){//如果返回null代表不支持 return newContext(set); }; var openList={id:0}; var newContext=function(setOrNull,_badW){ //独立运行的函数,scope.wkScope worker.onmessage 字符串会被替换 var run=function(e){ var ed=e.data; var wk_ctxs=scope.wkScope.wk_ctxs; var wk_lame=scope.wkScope.wk_lame; var wk_mp3TrimFix=scope.wkScope.wk_mp3TrimFix; var cur=wk_ctxs[ed.id]; if(ed.action=="init"){ wk_ctxs[ed.id]={ sampleRate:ed.sampleRate ,bitRate:ed.bitRate ,takeoff:ed.takeoff ,pcmSize:0 ,memory:new Int8Array(500000), mOffset:0 ,encObj:new wk_lame.Mp3Encoder(1,ed.sampleRate,ed.bitRate) }; }else if(!cur){ return; }; var addBytes=function(buf){ var bufLen=buf.length; if(cur.mOffset+bufLen>cur.memory.length){ var tmp=new Int8Array(cur.memory.length+Math.max(500000,bufLen)); tmp.set(cur.memory.subarray(0, cur.mOffset)); cur.memory=tmp; } cur.memory.set(buf,cur.mOffset); cur.mOffset+=bufLen; }; switch(ed.action){ case "stop": if(!cur.isCp) try{ cur.encObj.flush() }catch(e){ console.error(e) } cur.encObj=null; delete wk_ctxs[ed.id]; break; case "encode": if(cur.isCp)break; cur.pcmSize+=ed.pcm.length; try{ var buf=cur.encObj.encodeBuffer(ed.pcm); }catch(e){ //精简代码调用了abort cur.err=e; console.error(e); }; if(buf && buf.length>0){ if(cur.takeoff){ worker.onmessage({action:"takeoff",id:ed.id,chunk:buf}); }else{ addBytes(buf); }; }; break; case "complete": cur.isCp=1; try{ var buf=cur.encObj.flush(); }catch(e){ //精简代码调用了abort cur.err=e; console.error(e); }; if(buf && buf.length>0){ if(cur.takeoff){ worker.onmessage({action:"takeoff",id:ed.id,chunk:buf}); }else{ addBytes(buf); }; }; if(cur.err){ worker.onmessage({action:ed.action,id:ed.id ,err:"MP3 Encoder: "+cur.err.message}); break; }; var data=[cur.memory.buffer.slice(0,cur.mOffset)]; //去掉开头的标记信息帧 var meta=wk_mp3TrimFix.fn(data,cur.mOffset,cur.pcmSize,cur.sampleRate); worker.onmessage({ action:ed.action ,id:ed.id ,blob:data[0]||new ArrayBuffer(0) ,meta:meta }); break; }; }; var initOnMsg=function(isW){ worker.onmessage=function(e){ var data=e; if(isW)data=e.data; var ctx=openList[data.id]; if(ctx){ if(data.action=="takeoff"){ //取走实时生成的mp3数据 ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer)); }else{ //complete ctx.call&&ctx.call(data); ctx.call=null; }; }; }; }; var initCtx=function(){ var ctx={worker:worker,set:setOrNull}; if(setOrNull){ ctx.id=++openList.id; openList[ctx.id]=ctx; NormalizeSet(setOrNull); worker.postMessage({ action:"init" ,id:ctx.id ,sampleRate:setOrNull.sampleRate ,bitRate:setOrNull.bitRate ,takeoff:!!setOrNull.takeoffEncodeChunk ,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray }); }else{ worker.postMessage({ x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray }); }; return ctx; }; var scope,worker=mp3Worker; //非浏览器,不支持worker,或者开启失败,使用UI线程处理 if(_badW || !HasWebWorker){ Recorder.CLog($T("k9PT::当前环境不支持Web Worker,mp3实时编码器运行在主线程中"),3); worker={ postMessage:function(ed){ run({data:ed}); } }; scope={wkScope:{ wk_ctxs:{}, wk_lame:Recorder.lamejs, wk_mp3TrimFix:mp3TrimFix }}; initOnMsg(); return initCtx(); }; try{ if(!worker){ //创建一个新Worker var onmsg=(run+"").replace(/[\w\$]+\.onmessage/g,"self.postMessage"); onmsg=onmsg.replace(/[\w\$]+\.wkScope/g,"wkScope"); var jsCode=");wk_lame();self.onmessage="+onmsg; jsCode+=";var wkScope={ wk_ctxs:{},wk_lame:wk_lame"; jsCode+=",wk_mp3TrimFix:{rm:"+mp3TrimFix.rm+",fn:"+mp3TrimFix.fn+"} }"; var lamejsCode=Recorder.lamejs.toString(); var url=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_lame=(",lamejsCode,jsCode], {type:"text/javascript"})); worker=new Worker(url); setTimeout(function(){ (window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存 },10000);//chrome 83 file协议下如果直接释放,将会使WebWorker无法启动 initOnMsg(1); }; var ctx=initCtx(); ctx.isW=1; mp3Worker=worker; return ctx; }catch(e){//出错了就不要提供了 worker&&worker.terminate(); console.error(e); return newContext(setOrNull, 1);//切换到UI线程处理 }; }; Recorder.prototype.mp3_stop=function(startCtx){ if(startCtx&&startCtx.worker){ startCtx.worker.postMessage({ action:"stop" ,id:startCtx.id }); startCtx.worker=null; delete openList[startCtx.id]; //疑似泄露检测 排除id var opens=-1; for(var k in openList){ opens++; }; if(opens){ Recorder.CLog($T("fT6M::mp3 worker剩{1}个未stop",0,opens),3); }; }; }; Recorder.prototype.mp3_encode=function(startCtx,pcm){ if(startCtx&&startCtx.worker){ startCtx.worker.postMessage({ action:"encode" ,id:startCtx.id ,pcm:pcm }); }; }; Recorder.prototype.mp3_complete=function(startCtx,True,False,autoStop){ var This=this; if(startCtx&&startCtx.worker){ startCtx.call=function(data){ if(autoStop){ This.mp3_stop(startCtx); }; if(data.err){ False(data.err); }else{ mp3TrimFixSetMeta(data.meta,startCtx.set); True(data.blob,"audio/mp3"); }; }; startCtx.worker.postMessage({ action:"complete" ,id:startCtx.id }); }else{ False($T("mPxH::mp3编码器未start")); }; }; //*******辅助函数************ /*读取lamejs编码出来的mp3信息,只能读特定格式,如果读取失败返回null mp3Buffers=[ArrayBuffer,...] length=mp3Buffers的数据二进制总长度 */ Recorder.mp3ReadMeta=function(mp3Buffers,length){ //kill babel-polyfill ES6 Number.parseInt 不然放到Worker里面找不到方法,也不能用typeof(x)==object 会被替换成 _typeof var parseInt_ES3=typeof(window)!="undefined"&&window.parseInt||typeof(self)!="undefined"&&self.parseInt||parseInt; var u8arr0=new Uint8Array(mp3Buffers[0]||[]); if(u8arr0.length<4){ return null; }; var byteAt=function(idx,u8){ return ("0000000"+((u8||u8arr0)[idx]||0).toString(2)).substr(-8); }; var b2=byteAt(0)+byteAt(1); var b4=byteAt(2)+byteAt(3); if(!/^1{11}/.test(b2)){//未发现帧同步 return null; }; var version=({"00":2.5,"10":2,"11":1})[b2.substr(11,2)]; var layer=({"01":3})[b2.substr(13,2)];//仅支持Layer3 var sampleRate=({ //lamejs -> Tables.samplerate_table "1":[44100, 48000, 32000] ,"2":[22050, 24000, 16000] ,"2.5":[11025, 12000, 8000] })[version]; sampleRate&&(sampleRate=sampleRate[parseInt_ES3(b4.substr(4,2),2)]); var bitRate=[ //lamejs -> Tables.bitrate_table [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] //MPEG 2 2.5 ,[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]//MPEG 1 ][version==1?1:0][parseInt_ES3(b4.substr(0,4),2)]; if(!version || !layer || !bitRate || !sampleRate){ return null; }; var duration=Math.round(length*8/bitRate); var frame=layer==1?384:layer==2?1152:version==1?1152:576; var frameDurationFloat=frame/sampleRate*1000; var frameSize=Math.floor((frame*bitRate)/8/sampleRate*1000); //检测是否存在Layer3帧填充1字节。这里只获取第二帧的填充信息,首帧永远没有填充。其他帧可能隔一帧出现一个填充,或者隔很多帧出现一个填充;目测是取决于frameSize未舍入时的小数部分,因为有些采样率的frameSize会出现小数(11025、22050、44100 典型的除不尽),然后字节数无法表示这种小数,就通过一定步长来填充弥补小数部分丢失 var hasPadding=0,seek=0; for(var i=0;i<mp3Buffers.length;i++){ //寻找第二帧 var buf=mp3Buffers[i]; seek+=buf.byteLength; if(seek>=frameSize+3){ var buf8=new Uint8Array(buf); var idx=buf.byteLength-(seek-(frameSize+3)+1); var ib4=byteAt(idx,buf8); hasPadding=ib4.charAt(6)=="1"; break; }; }; if(hasPadding){ frameSize++; }; return { version:version //1 2 2.5 -> MPEG1 MPEG2 MPEG2.5 ,layer:layer//3 -> Layer3 ,sampleRate:sampleRate //采样率 hz ,bitRate:bitRate //比特率 kbps ,duration:duration //音频时长 ms ,size:length //总长度 byte ,hasPadding:hasPadding //是否存在1字节填充,首帧永远没有,这个值其实代表的第二帧是否有填充,并不代表其他帧的 ,frameSize:frameSize //每帧最大长度,含可能存在的1字节padding byte ,frameDurationFloat:frameDurationFloat //每帧时长,含小数 ms }; }; //去掉lamejs开头的标记信息帧,免得mp3解码出来的时长比pcm的长太多 var mp3TrimFix={//minfiy keep name rm:Recorder.mp3ReadMeta ,fn:function(mp3Buffers,length,pcmLength,pcmSampleRate){ var meta=this.rm(mp3Buffers,length); if(!meta){ return {size:length, err:"mp3 unknown format"}; }; var pcmDuration=Math.round(pcmLength/pcmSampleRate*1000); //开头多出这么多帧,移除掉;正常情况下最多为2帧 var num=Math.floor((meta.duration-pcmDuration)/meta.frameDurationFloat); if(num>0){ var size=num*meta.frameSize-(meta.hasPadding?1:0);//首帧没有填充,第二帧可能有填充,这里假设最多为2帧(测试并未出现3帧以上情况),其他帧不管,就算出现了并且导致了错误后面自动容错 length-=size; var arr0=0,arrs=[]; for(var i=0;i<mp3Buffers.length;i++){ var arr=mp3Buffers[i]; if(size<=0){ break; }; if(size>=arr.byteLength){ size-=arr.byteLength; arrs.push(arr); mp3Buffers.splice(i,1); i--; }else{ mp3Buffers[i]=arr.slice(size); arr0=arr; size=0; }; }; var checkMeta=this.rm(mp3Buffers,length); if(!checkMeta){ //还原变更,应该不太可能会出现 arr0&&(mp3Buffers[0]=arr0); for(var i=0;i<arrs.length;i++){ mp3Buffers.splice(i,0,arrs[i]); }; meta.err="mp3 fix error: 已还原,错误原因不明"; //worker里面没$T翻译 }; var fix=meta.trimFix={}; fix.remove=num; fix.removeDuration=Math.round(num*meta.frameDurationFloat); fix.duration=Math.round(length*8/meta.bitRate); }; return meta; } }; var mp3TrimFixSetMeta=function(meta,set){ var tag="MP3 Info: "; if(meta.sampleRate&&meta.sampleRate!=set.sampleRate || meta.bitRate&&meta.bitRate!=set.bitRate){ Recorder.CLog(tag+$T("uY9i::和设置的不匹配{1},已更新成{2}",0,"set:"+set.bitRate+"kbps "+set.sampleRate+"hz","set:"+meta.bitRate+"kbps "+meta.sampleRate+"hz"),3,set); set.sampleRate=meta.sampleRate; set.bitRate=meta.bitRate; }; var trimFix=meta.trimFix; if(trimFix){ tag+=$T("iMSm::Fix移除{1}帧",0,trimFix.remove)+" "+trimFix.removeDuration+"ms -> "+trimFix.duration+"ms"; if(trimFix.remove>2){ meta.err=(meta.err?meta.err+", ":"")+$T("b9zm::移除帧数过多"); }; }else{ tag+=(meta.duration||"-")+"ms"; }; if(meta.err){ Recorder.CLog(tag,meta.size?1:0,meta.err,meta); }else{ Recorder.CLog(tag,meta); }; }; }));