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编码解码

549 lines (489 loc) 16.2 kB
/* 录音 RecordApp: 微信小程序支持文件,支持在微信小程序环境中使用 https://github.com/xiangyuecn/Recorder 录音功能由微信小程序的RecorderManager录音接口提供(已屏蔽10分钟录音限制),因为js层已加载Recorder和相应的js编码引擎,所以,Recorder支持的录音格式,小程序内均可以做到支持。 */ (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 IsWx=typeof wx=="object" && !!wx.getRecorderManager; var App=Recorder.RecordApp; var CLog=App.CLog; var platform={ Support:function(call){ if(IsWx && isBrowser){ //有的h5里面有wx对象,又有的wx里面有window对象 var win=window,doc=win.document,loc=win.location,body=doc.body; if(loc && loc.href && loc.reload && body && body.appendChild){ CLog("识别是浏览器但又检测到wx",3); call(false); return; //多判断一些稳妥一点 } } call(IsWx); } ,CanProcess:function(){ return true;//支持实时回调 } }; App.RegisterPlatform("miniProgram-wx",platform); //当使用到录音的页面onShow时进行一次调用,用于恢复被暂停的录音(比如按了home键会暂停录音) App.MiniProgramWx_onShow=function(){ recOnShow(); }; /*******实现统一接口*******/ platform.RequestPermission=function(sid,success,fail){ requestPermission(success,fail); }; platform.Start=function(sid,set,success,fail){ onRecFn.param=set; var rec=Recorder(set); rec.set.disableEnvInFix=true; //不要音频输入丢失补偿 rec.dataType="arraybuffer"; onRecFn.rec=rec;//等待第一个数据到来再调用rec.start App.__Rec=rec;//App需要暴露出使用到的rec实例 recStart(success,fail); }; platform.Stop=function(sid,success,fail){ clearCurMg(); var failCall=function(msg){ if(App.__Sync(sid)){ onRecFn.rec=null; } fail(msg); }; var rec=onRecFn.rec; onRecFn.rec=null; var clearMsg=success?"":App.__StopOnlyClearMsg(); if(!rec){ failCall("未开始录音"+(clearMsg?" ("+clearMsg+")":"")); return; }; CLog("rec encode: pcm:"+rec.recSize+" srcSR:"+rec.srcSampleRate+" set:"+JSON.stringify(onRecFn.param)); var end=function(){ if(App.__Sync(sid)){ //把可能变更的配置写回去 for(var k in rec.set){ onRecFn.param[k]=rec.set[k]; }; }; }; if(!success){ end(); failCall(clearMsg); return; }; rec.stop(function(arrBuf,duration,mime){ end(); success(arrBuf,duration,mime); },function(msg){ end(); failCall(msg); }); }; var onRecFn=function(pcm,sampleRate){ var rec=onRecFn.rec; if(!rec){ CLog("未开始录音,但收到wx PCM数据",3); return; }; if(!rec._appStart){ rec.envStart({ envName:platform.Key,canProcess:platform.CanProcess() },sampleRate); }; rec._appStart=1; var sum=0; for(var i=0;i<pcm.length;i++){ sum+=Math.abs(pcm[i]); } rec.envIn(pcm,sum); }; /*******微信小程序录音接口调用*******/ var hasPermission=false; var requestPermission=function(success,fail){ clearCurMg(); initSys(); if(hasPermission){ success(); return; } var mg=wx.getRecorderManager(),next=1; mg.onStart(function(){ hasPermission=true; if(next){ next=0; stopMg(mg); success(); } }); mg.onError(function(res){ var msg="请求录音权限出现错误:"+res.errMsg; CLog(msg+"。"+UserPermissionMsg,1,res); if(next){ next=0; stopMg(mg); fail(msg,true); } }); newStart("req",mg); }; var UserPermissionMsg="请自行检查wx.getSetting中的scope.record录音权限,如果用户拒绝了权限,请引导用户到小程序设置中授予录音权限。"; var curMg,mgStime=0; var clearCurMg=function(){ var old=curMg;curMg=null; if(old){ stopMg(old) } }; var stopMg=function(mg){ mgStime=Date.now(); mg.stop(); }; var newStart=function(tag,mg){ //统一参数进行start调用,不然开发工具上热更新参数不一样直接卡死 var obj={ duration:600000 ,sampleRate:48000 //pc端无效 ,encodeBitRate:320000 ,numberOfChannels:1 ,format:"PCM" ,frameSize:isDev?1:4 //4=48/12 }; var set=onRecFn.param||{},aec=(set.audioTrackSet||{}).echoCancellation; if(sys.platform=="android"){ //Android指定麦克风源 MediaRecorder.AudioSource,0 DEFAULT 默认音频源,1 MIC 主麦克风,5 CAMCORDER 相机方向的麦,6 VOICE_RECOGNITION 语音识别,7 VOICE_COMMUNICATION 语音通信(带回声消除) var source=set.android_audioSource,asVal=""; if(source==null && aec) source=7; if(source==null) source=App.Default_Android_AudioSource; if(source==1) asVal="mic"; if(source==5) asVal="camcorder"; if(source==6) asVal="voice_recognition"; if(source==7) asVal="voice_communication"; if(asVal)obj.audioSource=asVal; }; if(aec){ CLog("mg注意:iOS下无法配置回声消除,Android无此问题,建议都启用听筒播放避免回声:wx.setInnerAudioOption({speakerOn:false})",3); }; CLog("["+tag+"]mg.start obj",obj); mg.start(obj); }; var recOnShow=function(){ if(curMg && curMg.__pause){ CLog("mg onShow 录音开始恢复...",3); curMg.resume(); } }; var recStart=function(success,fail){ clearCurMg(); initSys(); devWebMInfo={}; if(isDev){ CLog("RecorderManager.onFrameRecorded 在开发工具中测试返回的是webm格式音频,将会尝试进行解码。开发工具中录音偶尔会非常卡,建议使用真机测试(各种奇奇怪怪的毛病就都正常了)",3); } var startIsEnd=false,startCount=1; var startEnd=function(err){ if(startIsEnd)return; startIsEnd=true; if(err){ clearCurMg(); fail(err); }else{ success(); }; }; var mg=curMg=wx.getRecorderManager(); mg.onInterruptionEnd(function(){ if(mg!=curMg)return; CLog("mg onInterruptionEnd 录音开始恢复...",3); mg.resume(); }); mg.onPause(function(){ if(mg!=curMg)return; mg.__pause=Date.now(); CLog("mg onPause 录音被打断",3); }); mg.onResume(function(){ if(mg!=curMg)return; var t=mg.__pause?Date.now()-mg.__pause:0,t2=0; mg.__pause=0; if(t>300){//填充最多1秒的静默 t2=Math.min(1000,t); onRecFn(new Int16Array(48000/1000*t2),48000); } CLog("mg onResume 恢复录音,填充了"+t2+"ms静默",3); }); mg.onError(function(res){ if(mg!=curMg)return; var msg=res.errMsg,tag="mg onError 开始录音出错:"; if(!startIsEnd && !mg._srt && /fail.+is.+recording/i.test(msg)){ var st=600-(Date.now()-mgStime); //距离上次停止未超过600毫秒,重试 if(st>0){ st=Math.max(100,st); CLog(tag+"等待"+st+"ms重试",3,res); setTimeout(function(){ if(mg!=curMg)return; mg._srt=1; CLog(tag+"正在重试",3); newStart("retry start",mg); }, st); return; }; }; CLog(startCount>1?tag+"可能无法继续录音["+startCount+"]。"+msg :tag+msg+"。"+UserPermissionMsg,1,res); startEnd("开始录音出错:"+msg); }); mg.onStart(function(){ if(mg!=curMg)return; CLog("mg onStart 已开始录音"); mg._srt=0; //下次开始失败可以重试 mg._st=Date.now(); startEnd(); }); mg.onStop(function(res){ CLog("mg onStop 请勿尝试使用此原始结果中的文件路径(此原始文件的格式、采样率等和录音配置不相同);如需本地文件:可在RecordApp.Stop回调中将得到的ArrayBuffer(二进制音频数据)用RecordApp.MiniProgramWx_WriteLocalFile接口保存到本地,即可得到有效路径。res:",res); if(mg!=curMg)return; if(!mg._st || Date.now()-mg._st<600){ CLog("mg onStop但已忽略",3); return } CLog("mg onStop 已停止录音,正在重新开始录音..."); startCount++; mg._st=0; newStart("restart",mg); }); var start0=function(){ mg.onFrameRecorded(function(res){ if(mg!=curMg)return; if(!startIsEnd)CLog("mg onStart未触发,但收到了onFrameRecorded",3); startEnd(); var aBuf=res.frameBuffer; if(!aBuf || !aBuf.byteLength){ return; } if(isDev){ devWebmDecode(new Uint8Array(aBuf)); }else{ onRecFn(new Int16Array(aBuf),48000); }; }); newStart("start",mg); }; var st=600-(Date.now()-mgStime); //距离上次停止未超过600毫秒,等待一会,一般是第一次请求权限后立马开始录音造成的(录音参数不一样,不共享同一个mg) if(st>0){ st=Math.max(100,st); CLog("mg.start距stop太近需等待"+st+"ms",3); setTimeout(function(){ if(mg!=curMg)return; start0(); }, st); }else{ start0(); }; }; //保存文件到本地,提供文件名或set和arrayBuffer,True(savePath),False(errMsg) App.MiniProgramWx_WriteLocalFile=function(fileName,buffer,True,False){ var set=fileName; if(typeof(set)=="string") set={fileName:fileName}; fileName=set.fileName; var append=set.append; //追加写入到文件结尾 var seek_=set.seekOffset, seek=+seek_||0; //覆盖写入到指定位置 if(!seek_ && seek_!==0) seek=-1; var EnvUsr=wx.env.USER_DATA_PATH, savePath=fileName; if(fileName.indexOf(EnvUsr)==-1) savePath=EnvUsr+"/"+fileName; //如果上次还在写入,就等待,保证顺序写入 var tasks=writeTasks[savePath]=writeTasks[savePath]||[]; var tk0=tasks[0], tk={a:set,b:buffer,c:True,d:False}; if(tk0 && tk0._r){ //还在写入,等待 CLog("wx文件等待写入"+savePath,3); set._tk=1; tasks.push(tk); return; } if(set._tk) CLog("wx文件继续写入"+savePath); tasks.splice(0,0,tk); tk._r=1; //阻塞后续写入 var mg=wx.getFileSystemManager(), fd=0; var endCall=function(){ //操作完成 清理环境,延迟一下等操作完全结束 if(fd) mg.close({ fd:fd }); setTimeout(function(){ tasks.shift(); var tk=tasks.shift(); if(tk){ //继续写入等待的 App.MiniProgramWx_WriteLocalFile(tk.a,tk.b,tk.c,tk.d); } }); }; var okCall=function(){ endCall(); True&&True(savePath) }; var failCall=function(e){ endCall(); var msg=e.errMsg||"-"; CLog("wx文件"+savePath+"写入出错:"+msg,1); False&&False(msg); }; if(seek>-1 || append){ mg.open({ filePath:savePath ,flag:seek>-1?"r+":"a" ,success:function(res){ fd=res.fd; var opt={ fd:fd, data:buffer, success:okCall, fail:failCall }; if(seek>-1) opt.position=seek; mg.write(opt); } ,fail:failCall }); }else{ mg.writeFile({ filePath:savePath, encoding:"binary", data:buffer ,success:okCall, fail:failCall }); } }; var writeTasks={}; //删除已保存到本地的文件,savePath必须是WriteLocalFile得到的路径 True() False(errMsg) App.MiniProgramWx_DeleteLocalFile=function(savePath,True,False){ wx.getFileSystemManager().unlink({ filePath:savePath ,success:function(){ True&&True() } ,fail:function(e){ False&&False(e.errMsg||"-") } }); }; var isDev,sys; var initSys=function(){ if(sys)return; sys=wx.getSystemInfoSync(); isDev=sys.platform=="devtools"?1:0; if(isDev){ devWebCtx=wx.createWebAudioContext(); } }; /****开发工具内录音返回的webm数据解码成pcm,方便测试****/ var devWebCtx,devWebMInfo; //=======从WebM字节流中提取pcm数据===== var devWebmDecode=function(inBytes){ var scope=devWebMInfo; if(!scope.pos){ scope.pos=[0]; scope.tracks={}; scope.bytes=[]; }; var tracks=scope.tracks, position=[scope.pos[0]]; var endPos=function(){ scope.pos[0]=position[0] }; var sBL=scope.bytes.length; var bytes=new Uint8Array(sBL+inBytes.length); bytes.set(scope.bytes); bytes.set(inBytes,sBL); scope.bytes=bytes; //检测到不是webm,当做pcm直接返回 var returnPCM=function(){ scope.bytes=[]; onRecFn(new Int16Array(bytes),48000); }; if(scope.isNotWebM){ returnPCM(); return; }; //先读取文件头和Track信息 if(!scope._ht){ //暴力搜索EBML Header,开头数据可能存在上次录音结尾数据 var headPos0=0; for(var i=0;i<bytes.length;i++){ if(bytes[i]==0x1A && bytes[i+1]==0x45 && bytes[i+2]==0xDF && bytes[i+3]==0xA3){ headPos0=i; position[0]=i+4; break; } } if(!position[0]){ if(bytes.length>5*1024){ CLog("未识别到WebM数据,开发工具可能已支持PCM",3); scope.isNotWebM=true; returnPCM(); }; return;//未识别到EBML Header } readMatroskaBlock(bytes, position);//跳过EBML Header内容 if(!BytesEq(readMatroskaVInt(bytes, position), [0x18,0x53,0x80,0x67])){ return;//未识别到Segment } readMatroskaVInt(bytes, position);//跳过Segment长度值 while(position[0]<bytes.length){ var eid0=readMatroskaVInt(bytes, position); var bytes0=readMatroskaBlock(bytes, position); var pos0=[0],audioIdx=0; if(!bytes0)return;//数据不全,等待缓冲 //Track完整数据,循环读取TrackEntry if(BytesEq(eid0, [0x16,0x54,0xAE,0x6B])){ scope._ht=bytes.slice(headPos0,position[0]); CLog("WebM Tracks",tracks); endPos(); break; } } } //循环读取Cluster内的SimpleBlock var datas=[],dataLen=0; while(position[0]<bytes.length){ var p0=position[0]; var eid1=readMatroskaVInt(bytes, position); var p1=position[0]; var bytes1=readMatroskaBlock(bytes, position); if(!bytes1)break;//数据不全,等待缓冲 if(BytesEq(eid1, [0xA3])){//SimpleBlock完整数据 var arr=bytes.slice(p0,position[0]); dataLen+=arr.length; datas.push(arr); } endPos(); } if(!dataLen){ return; } var more=new Uint8Array(bytes.length-scope.pos[0]); more.set(bytes.subarray(scope.pos[0])); scope.bytes=more; //清理已读取了的缓冲数据 scope.pos[0]=0; //和头一起拼接成新的webm var add=[0x1F,0x43,0xB6,0x75,0x01,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF];//Cluster add.push(0xE7,0x81,0x00); dataLen+=add.length; datas.splice(0,0,add); dataLen+=scope._ht.length; datas.splice(0,0,scope._ht); var u8arr=new Uint8Array(dataLen); //已获取的音频数据 for(var i=0,i2=0;i<datas.length;i++){ u8arr.set(datas[i],i2); i2+=datas[i].length; } devWebCtx.decodeAudioData(u8arr.buffer, function(raw){ var src=raw.getChannelData(0); var pcm=new Int16Array(src.length); for(var i=0;i<src.length;i++){ var s=Math.max(-1,Math.min(1,src[i])); s=s<0?s*0x8000:s*0x7FFF; pcm[i]=s; }; onRecFn(pcm,raw.sampleRate); },function(){ CLog("WebM解码失败",1); }); }; //两个字节数组内容是否相同 var BytesEq=function(bytes1,bytes2){ if(!bytes1 || bytes1.length!=bytes2.length) return false; if(bytes1.length==1) return bytes1[0]==bytes2[0]; for(var i=0;i<bytes1.length;i++){ if(bytes1[i]!=bytes2[i]) return false; } return true; }; //字节数组BE转成int数字 var BytesInt=function(bytes){ var s="";//0-8字节,js位运算只支持4字节 for(var i=0;i<bytes.length;i++){var n=bytes[i];s+=(n<16?"0":"")+n.toString(16)}; return parseInt(s,16)||0; }; //读取一个可变长数值字节数组 var readMatroskaVInt=function(arr,pos,trim){ var i=pos[0]; if(i>=arr.length)return; var b0=arr[i],b2=("0000000"+b0.toString(2)).substr(-8); var m=/^(0*1)(\d*)$/.exec(b2); if(!m)return; var len=m[1].length, val=[]; if(i+len>arr.length)return; for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; } if(trim) val[0]=parseInt(m[2]||'0',2); pos[0]=i; return val; }; //读取一个自带长度的内容字节数组 var readMatroskaBlock=function(arr,pos){ var lenVal=readMatroskaVInt(arr,pos,1); if(!lenVal)return; var len=BytesInt(lenVal); var i=pos[0], val=[]; if(len<0x7FFFFFFF){ //超大值代表没有长度 if(i+len>arr.length)return; for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; } } pos[0]=i; return val; }; //=====End WebM读取===== }));