| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493 |
- /*
- 录音
- https://github.com/xiangyuecn/Recorder
- */
- (function(factory){
- factory(window);
- //umd returnExports.js
- if(typeof(define)=='function' && define.amd){
- define(function(){
- return Recorder;
- });
- };
- if(typeof(module)=='object' && module.exports){
- module.exports=Recorder;
- };
- }(function(window){
- "use strict";
- var NOOP=function(){};
- var Recorder=function(set){
- return new initFn(set);
- };
- Recorder.LM="2023-02-01 18:05";
- var RecTxt="Recorder";
- var getUserMediaTxt="getUserMedia";
- var srcSampleRateTxt="srcSampleRate";
- var sampleRateTxt="sampleRate";
- var CatchTxt="catch";
- //是否已经打开了全局的麦克风录音,所有工作都已经准备好了,就等接收音频数据了
- Recorder.IsOpen=function(){
- var stream=Recorder.Stream;
- if(stream){
- var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
- var track=tracks[0];
- if(track){
- var state=track.readyState;
- return state=="live"||state==track.LIVE;
- };
- };
- return false;
- };
- /*H5录音时的AudioContext缓冲大小。会影响H5录音时的onProcess调用速率,相对于AudioContext.sampleRate=48000时,4096接近12帧/s,调节此参数可生成比较流畅的回调动画。
- 取值256, 512, 1024, 2048, 4096, 8192, or 16384
- 注意,取值不能过低,2048开始不同浏览器可能回调速率跟不上造成音质问题。
- 一般无需调整,调整后需要先close掉已打开的录音,再open时才会生效。
- */
- Recorder.BufferSize=4096;
- //销毁已持有的所有全局资源,当要彻底移除Recorder时需要显式的调用此方法
- Recorder.Destroy=function(){
- CLog(RecTxt+" Destroy");
- Disconnect();//断开可能存在的全局Stream、资源
-
- for(var k in DestroyList){
- DestroyList[k]();
- };
- };
- var DestroyList={};
- //登记一个需要销毁全局资源的处理方法
- Recorder.BindDestroy=function(key,call){
- DestroyList[key]=call;
- };
- //判断浏览器是否支持录音,随时可以调用。注意:仅仅是检测浏览器支持情况,不会判断和调起用户授权,不会判断是否支持特定格式录音。
- Recorder.Support=function(){
- var scope=navigator.mediaDevices||{};
- if(!scope[getUserMediaTxt]){
- scope=navigator;
- scope[getUserMediaTxt]||(scope[getUserMediaTxt]=scope.webkitGetUserMedia||scope.mozGetUserMedia||scope.msGetUserMedia);
- };
- if(!scope[getUserMediaTxt]){
- return false;
- };
- Recorder.Scope=scope;
-
- if(!Recorder.GetContext()){
- return false;
- };
- return true;
- };
- //获取全局的AudioContext对象,如果浏览器不支持将返回null
- Recorder.GetContext=function(){
- var AC=window.AudioContext;
- if(!AC){
- AC=window.webkitAudioContext;
- };
- if(!AC){
- return null;
- };
-
- if(!Recorder.Ctx||Recorder.Ctx.state=="closed"){
- //不能反复构造,低版本number of hardware contexts reached maximum (6)
- Recorder.Ctx=new AC();
-
- Recorder.BindDestroy("Ctx",function(){
- var ctx=Recorder.Ctx;
- if(ctx&&ctx.close){//能关掉就关掉,关不掉就保留着
- ctx.close();
- Recorder.Ctx=0;
- };
- });
- };
- return Recorder.Ctx;
- };
- /*是否启用MediaRecorder.WebM.PCM来进行音频采集连接(如果浏览器支持的话),默认启用,禁用或者不支持时将使用AudioWorklet或ScriptProcessor来连接;MediaRecorder采集到的音频数据比其他方式更好,几乎不存在丢帧现象,所以音质明显会好很多,建议保持开启*/
- var ConnectEnableWebM="ConnectEnableWebM";
- Recorder[ConnectEnableWebM]=true;
- /*是否启用AudioWorklet特性来进行音频采集连接(如果浏览器支持的话),默认禁用,禁用或不支持时将使用过时的ScriptProcessor来连接(如果方法还在的话),当前AudioWorklet的实现在移动端没有ScriptProcessor稳健;ConnectEnableWebM如果启用并且有效时,本参数将不起作用*/
- var ConnectEnableWorklet="ConnectEnableWorklet";
- Recorder[ConnectEnableWorklet]=false;
- /*初始化H5音频采集连接。如果自行提供了sourceStream将只进行一次简单的连接处理。如果是普通麦克风录音,此时的Stream是全局的,Safari上断开后就无法再次进行连接使用,表现为静音,因此使用全部使用全局处理避免调用到disconnect;全局处理也有利于屏蔽底层细节,start时无需再调用底层接口,提升兼容、可靠性。*/
- var Connect=function(streamStore,isUserMedia){
- var bufferSize=streamStore.BufferSize||Recorder.BufferSize;
-
- var ctx=Recorder.Ctx,stream=streamStore.Stream;
- var mediaConn=function(node){
- var media=stream._m=ctx.createMediaStreamSource(stream);
- var ctxDest=ctx.destination,cmsdTxt="createMediaStreamDestination";
- if(ctx[cmsdTxt]){
- ctxDest=ctx[cmsdTxt]();
- };
- media.connect(node);
- node.connect(ctxDest);
- }
- var isWebM,isWorklet,badInt,webMTips="";
- var calls=stream._call;
-
- //浏览器回传的音频数据处理
- var onReceive=function(float32Arr){
- for(var k0 in calls){//has item
- var size=float32Arr.length;
-
- var pcm=new Int16Array(size);
- var sum=0;
- for(var j=0;j<size;j++){//floatTo16BitPCM
- var s=Math.max(-1,Math.min(1,float32Arr[j]));
- s=s<0?s*0x8000:s*0x7FFF;
- pcm[j]=s;
- sum+=Math.abs(s);
- };
-
- for(var k in calls){
- calls[k](pcm,sum);
- };
-
- return;
- };
- };
-
- var scriptProcessor="ScriptProcessor";//一堆字符串名字,有利于压缩js
- var audioWorklet="audioWorklet";
- var recAudioWorklet=RecTxt+" "+audioWorklet;
- var RecProc="RecProc";
- var MediaRecorderTxt="MediaRecorder";
- var MRWebMPCM=MediaRecorderTxt+".WebM.PCM";
- //===================连接方式三=========================
- //古董级别的 ScriptProcessor 处理,目前所有浏览器均兼容,虽然是过时的方法,但更稳健,移动端性能比AudioWorklet强
- var oldFn=ctx.createScriptProcessor||ctx.createJavaScriptNode;
- var oldIsBest="。由于"+audioWorklet+"内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启"+audioWorklet+"。";
- var oldScript=function(){
- isWorklet=stream.isWorklet=false;
- _Disconn_n(stream);
- CLog("Connect采用老的"+scriptProcessor+","+(Recorder[ConnectEnableWorklet]?"但已":"可")+"设置"+RecTxt+"."+ConnectEnableWorklet+"=true尝试启用"+audioWorklet+webMTips+oldIsBest,3);
-
- var process=stream._p=oldFn.call(ctx,bufferSize,1,1);//单声道,省的数据处理复杂
- mediaConn(process);
-
- var _DsetTxt="_D220626",_Dset=Recorder[_DsetTxt];if(_Dset)CLog("Use "+RecTxt+"."+_DsetTxt,3);
- process.onaudioprocess=function(e){
- var arr=e.inputBuffer.getChannelData(0);
- if(_Dset){//临时调试用的参数,未来会被删除
- arr=new Float32Array(arr);//块是共享的,必须复制出来
- setTimeout(function(){ onReceive(arr) });//立即退出回调,试图减少对浏览器录音的影响
- }else{
- onReceive(arr);
- };
- };
- };
- //===================连接方式二=========================
- var connWorklet=function(){
- //尝试开启AudioWorklet处理
- isWebM=stream.isWebM=false;
- _Disconn_r(stream);
-
- isWorklet=stream.isWorklet=!oldFn || Recorder[ConnectEnableWorklet];
- var AwNode=window.AudioWorkletNode;
- if(!(isWorklet && ctx[audioWorklet] && AwNode)){
- oldScript();//被禁用 或 不支持,直接使用老的
- return;
- };
- var clazzUrl=function(){
- var xf=function(f){return f.toString().replace(/^function|DEL_/g,"").replace(/\$RA/g,recAudioWorklet)};
- var clazz='class '+RecProc+' extends AudioWorkletProcessor{';
- clazz+="constructor "+xf(function(option){
- DEL_super(option);
- var This=this,bufferSize=option.processorOptions.bufferSize;
- This.bufferSize=bufferSize;
- This.buffer=new Float32Array(bufferSize*2);//乱给size搞乱缓冲区不管
- This.pos=0;
- This.port.onmessage=function(e){
- if(e.data.kill){
- This.kill=true;
- console.log("$RA kill call");
- }
- };
- console.log("$RA .ctor call", option);
- });
-
- //https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process 每次回调128个采样数据,1秒375次回调,高频导致移动端性能问题,结果就是回调次数缺斤少两,进而导致丢失数据,PC端似乎没有性能问题
- clazz+="process "+xf(function(input,b,c){//需要等到ctx激活后才会有回调
- var This=this,bufferSize=This.bufferSize;
- var buffer=This.buffer,pos=This.pos;
- input=(input[0]||[])[0]||[];
- if(input.length){
- buffer.set(input,pos);
- pos+=input.length;
-
- var len=~~(pos/bufferSize)*bufferSize;
- if(len){
- this.port.postMessage({ val: buffer.slice(0,len) });
-
- var more=buffer.subarray(len,pos);
- buffer=new Float32Array(bufferSize*2);
- buffer.set(more);
- pos=more.length;
- This.buffer=buffer;
- }
- This.pos=pos;
- }
- return !This.kill;
- });
- clazz+='}'
- +'try{'
- +'registerProcessor("'+RecProc+'", '+RecProc+')'
- +'}catch(e){'
- +'console.error("'+recAudioWorklet+'注册失败",e)'
- +'}';
- //URL.createObjectURL 本地有些浏览器会报 Not allowed to load local resource,直接用dataurl
- return "data:text/javascript;base64,"+btoa(unescape(encodeURIComponent(clazz)));
- };
-
- var awNext=function(){//可以继续,没有调用断开
- return isWorklet && stream._na;
- };
- var nodeAlive=stream._na=function(){
- //start时会调用,只要没有收到数据就断定AudioWorklet有问题,恢复用老的
- if(badInt!==""){//没有回调过数据
- clearTimeout(badInt);
- badInt=setTimeout(function(){
- badInt=0;
- if(awNext()){
- CLog(audioWorklet+"未返回任何音频,恢复使用"+scriptProcessor,3);
- oldFn&&oldScript();//未来没有老的,可能是误判
- };
- },500);
- };
- };
- var createNode=function(){
- if(!awNext())return;
- var node=stream._n=new AwNode(ctx, RecProc, {
- processorOptions:{bufferSize:bufferSize}
- });
- mediaConn(node);
- node.port.onmessage=function(e){
- if(badInt){
- clearTimeout(badInt);badInt="";
- };
- if(awNext()){
- onReceive(e.data.val);
- }else if(!isWorklet){
- CLog(audioWorklet+"多余回调",3);
- };
- };
- CLog("Connect采用"+audioWorklet+",设置"+RecTxt+"."+ConnectEnableWorklet+"=false可恢复老式"+scriptProcessor+webMTips+oldIsBest,3);
- };
-
- //如果start时的resume和下面的构造node同时进行,将会导致部分浏览器崩溃,源码assets中 ztest_chrome_bug_AudioWorkletNode.html 可测试。所以,将所有代码套到resume里面(不管catch),避免出现这个问题
- ctx.resume()[calls&&"finally"](function(){//注释掉这行 观摩浏览器崩溃 STATUS_ACCESS_VIOLATION
- if(!awNext())return;
- if(ctx[RecProc]){
- createNode();
- return;
- };
- var url=clazzUrl();
- ctx[audioWorklet].addModule(url).then(function(e){
- if(!awNext())return;
- ctx[RecProc]=1;
- createNode();
- if(badInt){//重新计时
- nodeAlive();
- };
- })[CatchTxt](function(e){ //fix 关键字,保证catch压缩时保持字符串形式
- CLog(audioWorklet+".addModule失败",1,e);
- awNext()&&oldScript();
- });
- });
- };
- //===================连接方式一=========================
- var connWebM=function(){
- //尝试开启MediaRecorder录制webm+pcm处理
- var MR=window[MediaRecorderTxt];
- var onData="ondataavailable";
- var webmType="audio/webm; codecs=pcm";
- isWebM=stream.isWebM=Recorder[ConnectEnableWebM];
-
- var supportMR=MR && (onData in MR.prototype) && MR.isTypeSupported(webmType);
- webMTips=supportMR?"":"(此浏览器不支持"+MRWebMPCM+")";
- if(!isUserMedia || !isWebM || !supportMR){
- connWorklet(); //非麦克风录音(MediaRecorder采样率不可控) 或 被禁用 或 不支持MediaRecorder 或 不支持webm+pcm
- return;
- }
-
- var mrNext=function(){//可以继续,没有调用断开
- return isWebM && stream._ra;
- };
- var mrAlive=stream._ra=function(){
- //start时会调用,只要没有收到数据就断定MediaRecorder有问题,降级处理
- if(badInt!==""){//没有回调过数据
- clearTimeout(badInt);
- badInt=setTimeout(function(){
- //badInt=0; 保留给nodeAlive继续判断
- if(mrNext()){
- CLog(MediaRecorderTxt+"未返回任何音频,降级使用"+audioWorklet,3);
- connWorklet();
- };
- },500);
- };
- };
-
- var mrSet=Object.assign({mimeType:webmType}, Recorder.ConnectWebMOptions);
- var mr=stream._r=new MR(stream, mrSet);
- var webmData=stream._rd={sampleRate:ctx[sampleRateTxt]};
- mr[onData]=function(e){
- //提取webm中的pcm数据,提取失败就等着badInt超时降级处理
- var reader=new FileReader();
- reader.onloadend=function(){
- if(mrNext()){
- var f32arr=WebM_Extract(new Uint8Array(reader.result),webmData);
- if(!f32arr)return;
- if(f32arr==-1){//无法提取,立即降级
- connWorklet();
- return;
- };
-
- if(badInt){
- clearTimeout(badInt);badInt="";
- };
- onReceive(f32arr);
- }else if(!isWebM){
- CLog(MediaRecorderTxt+"多余回调",3);
- };
- };
- reader.readAsArrayBuffer(e.data);
- };
- mr.start(~~(bufferSize/48));//按48k时的回调间隔
- CLog("Connect采用"+MRWebMPCM+",设置"+RecTxt+"."+ConnectEnableWebM+"=false可恢复使用"+audioWorklet+"或老式"+scriptProcessor);
- };
- connWebM();
- };
- var ConnAlive=function(stream){
- if(stream._na) stream._na(); //检查AudioWorklet连接是否有效,无效就回滚到老的ScriptProcessor
- if(stream._ra) stream._ra(); //检查MediaRecorder连接是否有效,无效就降级处理
- };
- var _Disconn_n=function(stream){
- stream._na=null;
- if(stream._n){
- stream._n.port.postMessage({kill:true});
- stream._n.disconnect();
- stream._n=null;
- };
- };
- var _Disconn_r=function(stream){
- stream._ra=null;
- if(stream._r){
- stream._r.stop();
- stream._r=null;
- };
- };
- var Disconnect=function(streamStore){
- streamStore=streamStore||Recorder;
- var isGlobal=streamStore==Recorder;
-
- var stream=streamStore.Stream;
- if(stream){
- if(stream._m){
- stream._m.disconnect();
- stream._m=null;
- };
- if(stream._p){
- stream._p.disconnect();
- stream._p.onaudioprocess=stream._p=null;
- };
- _Disconn_n(stream);
- _Disconn_r(stream);
-
- if(isGlobal){//全局的时候,要把流关掉(麦克风),直接提供的流不处理
- var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
- for(var i=0;i<tracks.length;i++){
- var track=tracks[i];
- track.stop&&track.stop();
- };
- stream.stop&&stream.stop();
- };
- };
- streamStore.Stream=0;
- };
- /*对pcm数据的采样率进行转换
- pcmDatas: [[Int16,...]] pcm片段列表
- pcmSampleRate:48000 pcm数据的采样率
- newSampleRate:16000 需要转换成的采样率,newSampleRate>=pcmSampleRate时不会进行任何处理,小于时会进行重新采样
- prevChunkInfo:{} 可选,上次调用时的返回值,用于连续转换,本次调用将从上次结束位置开始进行处理。或可自行定义一个ChunkInfo从pcmDatas指定的位置开始进行转换
- option:{ 可选,配置项
- frameSize:123456 帧大小,每帧的PCM Int16的数量,采样率转换后的pcm长度为frameSize的整数倍,用于连续转换。目前仅在mp3格式时才有用,frameSize取值为1152,这样编码出来的mp3时长和pcm的时长完全一致,否则会因为mp3最后一帧录音不够填满时添加填充数据导致mp3的时长变长。
- frameType:"" 帧类型,一般为rec.set.type,提供此参数时无需提供frameSize,会自动使用最佳的值给frameSize赋值,目前仅支持mp3=1152(MPEG1 Layer3的每帧采采样数),其他类型=1。
- 以上两个参数用于连续转换时使用,最多使用一个,不提供时不进行帧的特殊处理,提供时必须同时提供prevChunkInfo才有作用。最后一段数据处理时无需提供帧大小以便输出最后一丁点残留数据。
- }
- 返回ChunkInfo:{
- //可定义,从指定位置开始转换到结尾
- index:0 pcmDatas已处理到的索引
- offset:0.0 已处理到的index对应的pcm中的偏移的下一个位置
-
- //仅作为返回值
- frameNext:null||[Int16,...] 下一帧的部分数据,frameSize设置了的时候才可能会有
- sampleRate:16000 结果的采样率,<=newSampleRate
- data:[Int16,...] 转换后的PCM结果;如果是连续转换,并且pcmDatas中并没有新数据时,data的长度可能为0
- }
- */
- Recorder.SampleData=function(pcmDatas,pcmSampleRate,newSampleRate,prevChunkInfo,option){
- prevChunkInfo||(prevChunkInfo={});
- var index=prevChunkInfo.index||0;
- var offset=prevChunkInfo.offset||0;
-
- var frameNext=prevChunkInfo.frameNext||[];
- option||(option={});
- var frameSize=option.frameSize||1;
- if(option.frameType){
- frameSize=option.frameType=="mp3"?1152:1;
- };
-
- var nLen=pcmDatas.length;
- if(index>nLen+1){
- CLog("SampleData似乎传入了未重置chunk "+index+">"+nLen,3);
- };
- var size=0;
- for(var i=index;i<nLen;i++){
- size+=pcmDatas[i].length;
- };
- size=Math.max(0,size-Math.floor(offset));
-
- //采样 https://www.cnblogs.com/blqw/p/3782420.html
- var step=pcmSampleRate/newSampleRate;
- if(step>1){//新采样低于录音采样,进行抽样
- size=Math.floor(size/step);
- }else{//新采样高于录音采样不处理,省去了插值处理
- step=1;
- newSampleRate=pcmSampleRate;
- };
-
- size+=frameNext.length;
- var res=new Int16Array(size);
- var idx=0;
- //添加上一次不够一帧的剩余数据
- for(var i=0;i<frameNext.length;i++){
- res[idx]=frameNext[i];
- idx++;
- };
- //处理数据
- for (;index<nLen;index++) {
- var o=pcmDatas[index];
- var i=offset,il=o.length;
- while(i<il){
- //res[idx]=o[Math.round(i)]; 直接简单抽样
-
- //https://www.cnblogs.com/xiaoqi/p/6993912.html
- //当前点=当前点+到后面一个点之间的增量,音质比直接简单抽样好些
- var before = Math.floor(i);
- var after = Math.ceil(i);
- var atPoint = i - before;
-
- var beforeVal=o[before];
- var afterVal=after<il ? o[after]
- : (//后个点越界了,查找下一个数组
- (pcmDatas[index+1]||[beforeVal])[0]||0
- );
- res[idx]=beforeVal+(afterVal-beforeVal)*atPoint;
-
- idx++;
- i+=step;//抽样
- };
- offset=i-il;
- };
- //帧处理
- frameNext=null;
- var frameNextSize=res.length%frameSize;
- if(frameNextSize>0){
- var u8Pos=(res.length-frameNextSize)*2;
- frameNext=new Int16Array(res.buffer.slice(u8Pos));
- res=new Int16Array(res.buffer.slice(0,u8Pos));
- };
-
- return {
- index:index
- ,offset:offset
-
- ,frameNext:frameNext
- ,sampleRate:newSampleRate
- ,data:res
- };
- };
- /*计算音量百分比的一个方法
- pcmAbsSum: pcm Int16所有采样的绝对值的和
- pcmLength: pcm长度
- 返回值:0-100,主要当做百分比用
- 注意:这个不是分贝,因此没用volume当做名称*/
- Recorder.PowerLevel=function(pcmAbsSum,pcmLength){
- /*计算音量 https://blog.csdn.net/jody1989/article/details/73480259
- 更高灵敏度算法:
- 限定最大感应值10000
- 线性曲线:低音量不友好
- power/10000*100
- 对数曲线:低音量友好,但需限定最低感应值
- (1+Math.log10(power/10000))*100
- */
- var power=(pcmAbsSum/pcmLength) || 0;//NaN
- var level;
- if(power<1251){//1250的结果10%,更小的音量采用线性取值
- level=Math.round(power/1250*10);
- }else{
- level=Math.round(Math.min(100,Math.max(0,(1+Math.log(power/10000)/Math.log(10))*100)));
- };
- return level;
- };
- /*计算音量,单位dBFS(满刻度相对电平)
- maxSample: 为16位pcm采样的绝对值中最大的一个(计算峰值音量),或者为pcm中所有采样的绝对值的平局值
- 返回值:-100~0 (最大值0dB,最小值-100代替-∞)
- */
- Recorder.PowerDBFS=function(maxSample){
- var val=Math.max(0.1, maxSample||0),Pref=0x7FFF;
- val=Math.min(val,Pref);
- //https://www.logiclocmusic.com/can-you-tell-the-decibel/
- //https://blog.csdn.net/qq_17256689/article/details/120442510
- val=20*Math.log(val/Pref)/Math.log(10);
- return Math.max(-100,Math.round(val));
- };
- //带时间的日志输出,可设为一个空函数来屏蔽日志输出
- //CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn,否则当做内容输出,第一个参数不能是对象因为要拼接时间,后面可以接无数个输出参数
- Recorder.CLog=function(msg,err){
- var now=new Date();
- var t=("0"+now.getMinutes()).substr(-2)
- +":"+("0"+now.getSeconds()).substr(-2)
- +"."+("00"+now.getMilliseconds()).substr(-3);
- var recID=this&&this.envIn&&this.envCheck&&this.id;
- var arr=["["+t+" "+RecTxt+(recID?":"+recID:"")+"]"+msg];
- var a=arguments,console=window.console||{};
- var i=2,fn=console.log;
- if(typeof(err)=="number"){
- fn=err==1?console.error:err==3?console.warn:fn;
- }else{
- i=1;
- };
- for(;i<a.length;i++){
- arr.push(a[i]);
- };
- if(IsLoser){//古董浏览器,仅保证基本的可执行不代码异常
- fn&&fn("[IsLoser]"+arr[0],arr.length>1?arr:"");
- }else{
- fn.apply(console,arr);
- };
- };
- var CLog=function(){ Recorder.CLog.apply(this,arguments); };
- var IsLoser=true;try{IsLoser=!console.log.apply;}catch(e){};
- var ID=0;
- function initFn(set){
- this.id=++ID;
-
- //如果开启了流量统计,这里将发送一个图片请求
- Traffic();
-
-
- var o={
- type:"mp3" //输出类型:mp3,wav,wav输出文件尺寸超大不推荐使用,但mp3编码支持会导致js文件超大,如果不需支持mp3可以使js文件大幅减小
- ,bitRate:16 //比特率 wav:16或8位,MP3:8kbps 1k/s,8kbps 2k/s 录音文件很小
-
- ,sampleRate:16000 //采样率,wav格式大小=sampleRate*时间;mp3此项对低比特率有影响,高比特率几乎无影响。
- //wav任意值,mp3取值范围:48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000
- //采样率参考https://www.cnblogs.com/devin87/p/mp3-recorder.html
-
- ,onProcess:NOOP //fn(buffers,powerLevel,bufferDuration,bufferSampleRate,newBufferIdx,asyncEnd) buffers=[[Int16,...],...]:缓冲的PCM数据,为从开始录音到现在的所有pcm片段;powerLevel:当前缓冲的音量级别0-100,bufferDuration:已缓冲时长,bufferSampleRate:缓冲使用的采样率(当type支持边录边转码(Worker)时,此采样率和设置的采样率相同,否则不一定相同);newBufferIdx:本次回调新增的buffer起始索引;asyncEnd:fn() 如果onProcess是异步的(返回值为true时),处理完成时需要调用此回调,如果不是异步的请忽略此参数,此方法回调时必须是真异步(不能真异步时需用setTimeout包裹)。onProcess返回值:如果返回true代表开启异步模式,在某些大量运算的场合异步是必须的,必须在异步处理完成时调用asyncEnd(不能真异步时需用setTimeout包裹),在onProcess执行后新增的buffer会全部替换成空数组,因此本回调开头应立即将newBufferIdx到本次回调结尾位置的buffer全部保存到另外一个数组内,处理完成后写回buffers中本次回调的结尾位置。
-
- //*******高级设置******
- //,sourceStream:MediaStream Object
- //可选直接提供一个媒体流,从这个流中录制、实时处理音频数据(当前Recorder实例独享此流);不提供时为普通的麦克风录音,由getUserMedia提供音频流(所有Recorder实例共享同一个流)
- //比如:audio、video标签dom节点的captureStream方法(实验特性,不同浏览器支持程度不高)返回的流;WebRTC中的remote流;自己创建的流等
- //注意:流内必须至少存在一条音轨(Audio Track),比如audio标签必须等待到可以开始播放后才会有音轨,否则open会失败
-
- //,audioTrackSet:{ deviceId:"",groupId:"", autoGainControl:true, echoCancellation:true, noiseSuppression:true }
- //普通麦克风录音时getUserMedia方法的audio配置参数,比如指定设备id,回声消除、降噪开关;注意:提供的任何配置值都不一定会生效
- //由于麦克风是全局共享的,所以新配置后需要close掉以前的再重新open
- //更多参考: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
-
- //,disableEnvInFix:false 内部参数,禁用设备卡顿时音频输入丢失补偿功能
-
- //,takeoffEncodeChunk:NOOP //fn(chunkBytes) chunkBytes=[Uint8,...]:实时编码环境下接管编码器输出,当编码器实时编码出一块有效的二进制音频数据时实时回调此方法;参数为二进制的Uint8Array,就是编码出来的音频数据片段,所有的chunkBytes拼接在一起即为完整音频。本实现的想法最初由QQ2543775048提出
- //当提供此回调方法时,将接管编码器的数据输出,编码器内部将放弃存储生成的音频数据;环境要求比较苛刻:如果当前环境不支持实时编码处理,将在open时直接走fail逻辑
- //因此提供此回调后调用stop方法将无法获得有效的音频数据,因为编码器内没有音频数据,因此stop时返回的blob将是一个字节长度为0的blob
- //目前只有mp3格式实现了实时编码,在支持实时处理的环境中将会实时的将编码出来的mp3片段通过此方法回调,所有的chunkBytes拼接到一起即为完整的mp3,此种拼接的结果比mock方法实时生成的音质更加,因为天然避免了首尾的静默
- //目前除mp3外其他格式不可以提供此回调,提供了将在open时直接走fail逻辑
- };
-
- for(var k in set){
- o[k]=set[k];
- };
- this.set=o;
-
- this._S=9;//stop同步锁,stop可以阻止open过程中还未运行的start
- this.Sync={O:9,C:9};//和Recorder.Sync一致,只不过这个是非全局的,仅用来简化代码逻辑,无实际作用
- };
- //同步锁,控制对Stream的竞争;用于close时中断异步的open;一个对象open如果变化了都要阻止close,Stream的控制权交个新的对象
- Recorder.Sync={/*open*/O:9,/*close*/C:9};
- Recorder.prototype=initFn.prototype={
- CLog:CLog
-
- //流相关的数据存储在哪个对象里面;如果提供了sourceStream,数据直接存储在当前对象中,否则存储在全局
- ,_streamStore:function(){
- if(this.set.sourceStream){
- return this;
- }else{
- return Recorder;
- }
- }
-
- //打开录音资源True(),False(msg,isUserNotAllow),需要调用close。注意:此方法是异步的;一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音
- ,open:function(True,False){
- var This=this,streamStore=This._streamStore();
- True=True||NOOP;
- var failCall=function(errMsg,isUserNotAllow){
- isUserNotAllow=!!isUserNotAllow;
- This.CLog("录音open失败:"+errMsg+",isUserNotAllow:"+isUserNotAllow,1);
- False&&False(errMsg,isUserNotAllow);
- };
-
- var ok=function(){
- This.CLog("open ok id:"+This.id);
- True();
-
- This._SO=0;//解除stop对open中的start调用的阻止
- };
-
-
- //同步锁
- var Lock=streamStore.Sync;
- var lockOpen=++Lock.O,lockClose=Lock.C;
- This._O=This._O_=lockOpen;//记住当前的open,如果变化了要阻止close,这里假定了新对象已取代当前对象并且不再使用
- This._SO=This._S;//记住open过程中的stop,中途任何stop调用后都不能继续open中的start
- var lockFail=function(){
- //允许多次open,但不允许任何一次close,或者自身已经调用了关闭
- if(lockClose!=Lock.C || !This._O){
- var err="open被取消";
- if(lockOpen==Lock.O){
- //无新的open,已经调用了close进行取消,此处应让上次的close明确生效
- This.close();
- }else{
- err="open被中断";
- };
- failCall(err);
- return true;
- };
- };
-
- //环境配置检查
- var checkMsg=This.envCheck({envName:"H5",canProcess:true});
- if(checkMsg){
- failCall("不能录音:"+checkMsg);
- return;
- };
-
-
- //***********已直接提供了音频流************
- if(This.set.sourceStream){
- if(!Recorder.GetContext()){
- failCall("不支持此浏览器从流中获取录音");
- return;
- };
-
- Disconnect(streamStore);//可能已open过,直接先尝试断开
- This.Stream=This.set.sourceStream;
- This.Stream._call={};
-
- try{
- Connect(streamStore);
- }catch(e){
- failCall("从流中打开录音失败:"+e.message);
- return;
- }
- ok();
- return;
- };
-
-
- //***********打开麦克风得到全局的音频流************
- var codeFail=function(code,msg){
- try{//跨域的优先检测一下
- window.top.a;
- }catch(e){
- failCall('无权录音(跨域,请尝试给iframe添加麦克风访问策略,如allow="camera;microphone")');
- return;
- };
-
- if(/Permission|Allow/i.test(code)){
- failCall("用户拒绝了录音权限",true);
- }else if(window.isSecureContext===false){
- failCall("浏览器禁止不安全页面录音,可开启https解决");
- }else if(/Found/i.test(code)){//可能是非安全环境导致的没有设备
- failCall(msg+",无可用麦克风");
- }else{
- failCall(msg);
- };
- };
-
-
- //如果已打开并且有效就不要再打开了
- if(Recorder.IsOpen()){
- ok();
- return;
- };
- if(!Recorder.Support()){
- codeFail("","此浏览器不支持录音");
- return;
- };
-
- //请求权限,如果从未授权,一般浏览器会弹出权限请求弹框
- var f1=function(stream){
- //https://github.com/xiangyuecn/Recorder/issues/14 获取到的track.readyState!="live",刚刚回调时可能是正常的,但过一下可能就被关掉了,原因不明。延迟一下保证真异步。对正常浏览器不影响
- setTimeout(function(){
- stream._call={};
- var oldStream=Recorder.Stream;
- if(oldStream){
- Disconnect(); //直接断开已存在的,旧的Connect未完成会自动终止
- stream._call=oldStream._call;
- };
- Recorder.Stream=stream;
- if(lockFail())return;
-
- if(Recorder.IsOpen()){
- if(oldStream)This.CLog("发现同时多次调用open",1);
-
- Connect(streamStore,1);
- ok();
- }else{
- failCall("录音功能无效:无音频流");
- };
- },100);
- };
- var f2=function(e){
- var code=e.name||e.message||e.code+":"+e;
- This.CLog("请求录音权限错误",1,e);
-
- codeFail(code,"无法录音:"+code);
- };
-
- var trackSet={
- noiseSuppression:false //默认禁用降噪,原声录制,免得移动端表现怪异(包括系统播放声音变小)
- ,echoCancellation:false //回声消除
- };
- var trackSet2=This.set.audioTrackSet;
- for(var k in trackSet2)trackSet[k]=trackSet2[k];
- trackSet.sampleRate=Recorder.Ctx.sampleRate;//必须指明采样率,不然手机上MediaRecorder采样率16k
-
- try{
- var pro=Recorder.Scope[getUserMediaTxt]({audio:trackSet},f1,f2);
- }catch(e){//不能设置trackSet就算了
- This.CLog(getUserMediaTxt,3,e);
- pro=Recorder.Scope[getUserMediaTxt]({audio:true},f1,f2);
- };
- if(pro&&pro.then){
- pro.then(f1)[CatchTxt](f2); //fix 关键字,保证catch压缩时保持字符串形式
- };
- }
- //关闭释放录音资源
- ,close:function(call){
- call=call||NOOP;
-
- var This=this,streamStore=This._streamStore();
- This._stop();
-
- var Lock=streamStore.Sync;
- This._O=0;
- if(This._O_!=Lock.O){
- //唯一资源Stream的控制权已交给新对象,这里不能关闭。此处在每次都弹权限的浏览器内可能存在泄漏,新对象被拒绝权限可能不会调用close,忽略这种不处理
- This.CLog("close被忽略(因为同时open了多个rec,只有最后一个会真正close)",3);
- call();
- return;
- };
- Lock.C++;//获得控制权
-
- Disconnect(streamStore);
-
- This.CLog("close");
- call();
- }
-
-
-
-
-
- /*模拟一段录音数据,后面可以调用stop进行编码,需提供pcm数据[1,2,3...],pcm的采样率*/
- ,mock:function(pcmData,pcmSampleRate){
- var This=this;
- This._stop();//清理掉已有的资源
-
- This.isMock=1;
- This.mockEnvInfo=null;
- This.buffers=[pcmData];
- This.recSize=pcmData.length;
- This[srcSampleRateTxt]=pcmSampleRate;
- return This;
- }
- ,envCheck:function(envInfo){//平台环境下的可用性检查,任何时候都可以调用检查,返回errMsg:""正常,"失败原因"
- //envInfo={envName:"H5",canProcess:true}
- var errMsg,This=this,set=This.set;
-
- //检测CPU的数字字节序,TypedArray字节序是个迷,直接拒绝罕见的大端模式,因为找不到这种CPU进行测试
- var tag="CPU_BE";
- if(!errMsg && !Recorder[tag] && window.Int8Array && !new Int8Array(new Int32Array([1]).buffer)[0]){
- Traffic(tag); //如果开启了流量统计,这里将发送一个图片请求
- errMsg="不支持"+tag+"架构";
- };
-
- //编码器检查环境下配置是否可用
- if(!errMsg){
- var type=set.type;
- if(This[type+"_envCheck"]){//编码器已实现环境检查
- errMsg=This[type+"_envCheck"](envInfo,set);
- }else{//未实现检查的手动检查配置是否有效
- if(set.takeoffEncodeChunk){
- errMsg=type+"类型"+(This[type]?"":"(未加载编码器)")+"不支持设置takeoffEncodeChunk";
- };
- };
- };
-
- return errMsg||"";
- }
- ,envStart:function(mockEnvInfo,sampleRate){//平台环境相关的start调用
- var This=this,set=This.set;
- This.isMock=mockEnvInfo?1:0;//非H5环境需要启用mock,并提供envCheck需要的环境信息
- This.mockEnvInfo=mockEnvInfo;
- This.buffers=[];//数据缓冲
- This.recSize=0;//数据大小
-
- This.envInLast=0;//envIn接收到最后录音内容的时间
- This.envInFirst=0;//envIn接收到的首个录音内容的录制时间
- This.envInFix=0;//补偿的总时间
- This.envInFixTs=[];//补偿计数列表
-
- //engineCtx需要提前确定最终的采样率
- var setSr=set[sampleRateTxt];
- if(setSr>sampleRate){
- set[sampleRateTxt]=sampleRate;
- }else{ setSr=0 }
- This[srcSampleRateTxt]=sampleRate;
- This.CLog(srcSampleRateTxt+": "+sampleRate+" set."+sampleRateTxt+": "+set[sampleRateTxt]+(setSr?" 忽略"+setSr:""), setSr?3:0);
-
- This.engineCtx=0;
- //此类型有边录边转码(Worker)支持
- if(This[set.type+"_start"]){
- var engineCtx=This.engineCtx=This[set.type+"_start"](set);
- if(engineCtx){
- engineCtx.pcmDatas=[];
- engineCtx.pcmSize=0;
- };
- };
- }
- ,envResume:function(){//和平台环境无关的恢复录音
- //重新开始计数
- this.envInFixTs=[];
- }
- ,envIn:function(pcm,sum){//和平台环境无关的pcm[Int16]输入
- var This=this,set=This.set,engineCtx=This.engineCtx;
- var bufferSampleRate=This[srcSampleRateTxt];
- var size=pcm.length;
- var powerLevel=Recorder.PowerLevel(sum,size);
-
- var buffers=This.buffers;
- var bufferFirstIdx=buffers.length;//之前的buffer都是经过onProcess处理好的,不允许再修改
- buffers.push(pcm);
-
- //有engineCtx时会被覆盖,这里保存一份
- var buffersThis=buffers;
- var bufferFirstIdxThis=bufferFirstIdx;
-
- //卡顿丢失补偿:因为设备很卡的时候导致H5接收到的数据量不够造成播放时候变速,结果比实际的时长要短,此处保证了不会变短,但不能修复丢失的音频数据造成音质变差。当前算法采用输入时间侦测下一帧是否需要添加补偿帧,需要(6次输入||超过1秒)以上才会开始侦测,如果滑动窗口内丢失超过1/3就会进行补偿
- var now=Date.now();
- var pcmTime=Math.round(size/bufferSampleRate*1000);
- This.envInLast=now;
- if(This.buffers.length==1){//记下首个录音数据的录制时间
- This.envInFirst=now-pcmTime;
- };
- var envInFixTs=This.envInFixTs;
- envInFixTs.splice(0,0,{t:now,d:pcmTime});
- //保留3秒的计数滑动窗口,另外超过3秒的停顿不补偿
- var tsInStart=now,tsPcm=0;
- for(var i=0;i<envInFixTs.length;i++){
- var o=envInFixTs[i];
- if(now-o.t>3000){
- envInFixTs.length=i;
- break;
- };
- tsInStart=o.t;
- tsPcm+=o.d;
- };
- //达到需要的数据量,开始侦测是否需要补偿
- var tsInPrev=envInFixTs[1];
- var tsIn=now-tsInStart;
- var lost=tsIn-tsPcm;
- if( lost>tsIn/3 && (tsInPrev&&tsIn>1000 || envInFixTs.length>=6) ){
- //丢失过多,开始执行补偿
- var addTime=now-tsInPrev.t-pcmTime;//距离上次输入丢失这么多ms
- if(addTime>pcmTime/5){//丢失超过本帧的1/5
- var fixOpen=!set.disableEnvInFix;
- This.CLog("["+now+"]"+(fixOpen?"":"未")+"补偿"+addTime+"ms",3);
- This.envInFix+=addTime;
-
- //用静默进行补偿
- if(fixOpen){
- var addPcm=new Int16Array(addTime*bufferSampleRate/1000);
- size+=addPcm.length;
- buffers.push(addPcm);
- };
- };
- };
-
-
- var sizeOld=This.recSize,addSize=size;
- var bufferSize=sizeOld+addSize;
- This.recSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
-
-
- //此类型有边录边转码(Worker)支持,开启实时转码
- if(engineCtx){
- //转换成set的采样率
- var chunkInfo=Recorder.SampleData(buffers,bufferSampleRate,set[sampleRateTxt],engineCtx.chunkInfo);
- engineCtx.chunkInfo=chunkInfo;
-
- sizeOld=engineCtx.pcmSize;
- addSize=chunkInfo.data.length;
- bufferSize=sizeOld+addSize;
- engineCtx.pcmSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
-
- buffers=engineCtx.pcmDatas;
- bufferFirstIdx=buffers.length;
- buffers.push(chunkInfo.data);
- bufferSampleRate=chunkInfo[sampleRateTxt];
- };
-
- var duration=Math.round(bufferSize/bufferSampleRate*1000);
- var bufferNextIdx=buffers.length;
- var bufferNextIdxThis=buffersThis.length;
-
- //允许异步处理buffer数据
- var asyncEnd=function(){
- //重新计算size,异步的早已减去添加的,同步的需去掉本次添加的然后重新计算
- var num=asyncBegin?0:-addSize;
- var hasClear=buffers[0]==null;
- for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
- var buffer=buffers[i];
- if(buffer==null){//已被主动释放内存,比如长时间实时传输录音时
- hasClear=1;
- }else{
- num+=buffer.length;
-
- //推入后台边录边转码
- if(engineCtx&&buffer.length){
- This[set.type+"_encode"](engineCtx,buffer);
- };
- };
- };
-
- //同步清理This.buffers,不管buffers到底清了多少个,buffersThis是使用不到的进行全清
- if(hasClear && engineCtx){
- var i=bufferFirstIdxThis;
- if(buffersThis[0]){
- i=0;
- };
- for(;i<bufferNextIdxThis;i++){
- buffersThis[i]=null;
- };
- };
-
- //统计修改后的size,如果异步发生clear要原样加回来,同步的无需操作
- if(hasClear){
- num=asyncBegin?addSize:0;
-
- buffers[0]=null;//彻底被清理
- };
- if(engineCtx){
- engineCtx.pcmSize+=num;
- }else{
- This.recSize+=num;
- };
- };
- //实时回调处理数据,允许修改或替换上次回调以来新增的数据 ,但是不允许修改已处理过的,不允许增删第一维数组 ,允许将第二维数组任意修改替换成空数组也可以
- var asyncBegin=0,procTxt="rec.set.onProcess";
- try{
- asyncBegin=set.onProcess(buffers,powerLevel,duration,bufferSampleRate,bufferFirstIdx,asyncEnd);
- }catch(e){
- //此错误显示不要用CLog,这样控制台内相同内容不会重复打印
- console.error(procTxt+"回调出错是不允许的,需保证不会抛异常",e);
- };
-
- var slowT=Date.now()-now;
- if(slowT>10 && This.envInFirst-now>1000){ //1秒后开始onProcess性能监测
- This.CLog(procTxt+"低性能,耗时"+slowT+"ms",3);
- };
-
- if(asyncBegin===true){
- //开启了异步模式,onProcess已接管buffers新数据,立即清空,避免出现未处理的数据
- var hasClear=0;
- for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
- if(buffers[i]==null){//已被主动释放内存,比如长时间实时传输录音时 ,但又要开启异步模式,此种情况是非法的
- hasClear=1;
- }else{
- buffers[i]=new Int16Array(0);
- };
- };
-
- if(hasClear){
- This.CLog("未进入异步前不能清除buffers",3);
- }else{
- //还原size,异步结束后再统计仅修改后的size,如果发生clear要原样加回来
- if(engineCtx){
- engineCtx.pcmSize-=addSize;
- }else{
- This.recSize-=addSize;
- };
- };
- }else{
- asyncEnd();
- };
- }
-
-
-
-
- //开始录音,需先调用open;只要open成功时,调用此方法是安全的,如果未open强行调用导致的内部错误将不会有任何提示,stop时自然能得到错误
- ,start:function(){
- var This=this,ctx=Recorder.Ctx;
-
- var isOpen=1;
- if(This.set.sourceStream){//直接提供了流,仅判断是否调用了open
- if(!This.Stream){
- isOpen=0;
- }
- }else if(!Recorder.IsOpen()){//监测全局麦克风是否打开并且有效
- isOpen=0;
- };
- if(!isOpen){
- This.CLog("未open",1);
- return;
- };
- This.CLog("开始录音");
-
- This._stop();
- This.state=3;//0未录音 1录音中 2暂停 3等待ctx激活
- This.envStart(null, ctx[sampleRateTxt]);
-
- //检查open过程中stop是否已经调用过
- if(This._SO&&This._SO+1!=This._S){//上面调用过一次 _stop
- //open未完成就调用了stop,此种情况终止start。也应尽量避免出现此情况
- This.CLog("start被中断",3);
- return;
- };
- This._SO=0;
-
- var end=function(){
- if(This.state==3){
- This.state=1;
- This.resume();
- }
- };
- if(ctx.state=="suspended"){
- var tag="AudioContext resume: ";
- This.CLog(tag+"wait...");
- ctx.resume().then(function(){
- This.CLog(tag+ctx.state);
- end();
- })[CatchTxt](function(e){ //比较少见,可能对录音没有影响
- This.CLog(tag+ctx.state+" 可能无法录音:"+e.message,1,e);
- end();
- });
- }else{
- end();
- };
- }
- /*暂停录音*/
- ,pause:function(){
- var This=this;
- if(This.state){
- This.state=2;
- This.CLog("pause");
- delete This._streamStore().Stream._call[This.id];
- };
- }
- /*恢复录音*/
- ,resume:function(){
- var This=this;
- if(This.state){
- This.state=1;
- This.CLog("resume");
- This.envResume();
-
- var stream=This._streamStore().Stream;
- stream._call[This.id]=function(pcm,sum){
- if(This.state==1){
- This.envIn(pcm,sum);
- };
- };
- ConnAlive(stream);//AudioWorklet只会在ctx激活后运行
- };
- }
-
-
-
-
- ,_stop:function(keepEngine){
- var This=this,set=This.set;
- if(!This.isMock){
- This._S++;
- };
- if(This.state){
- This.pause();
- This.state=0;
- };
- if(!keepEngine && This[set.type+"_stop"]){
- This[set.type+"_stop"](This.engineCtx);
- This.engineCtx=0;
- };
- }
- /*
- 结束录音并返回录音数据blob对象
- True(blob,duration) blob:录音数据audio/mp3|wav格式
- duration:录音时长,单位毫秒
- False(msg)
- autoClose:false 可选,是否自动调用close,默认为false
- */
- ,stop:function(True,False,autoClose){
- var This=this,set=This.set,t1;
- var envInMS=This.envInLast-This.envInFirst, envInLen=envInMS&&This.buffers.length; //可能未start
- This.CLog("stop 和start时差"+(envInMS?envInMS+"ms 补偿"+This.envInFix+"ms"+" envIn:"+envInLen+" fps:"+(envInLen/envInMS*1000).toFixed(1):"-"));
-
- var end=function(){
- This._stop();//彻底关掉engineCtx
- if(autoClose){
- This.close();
- };
- };
- var err=function(msg){
- This.CLog("结束录音失败:"+msg,1);
- False&&False(msg);
- end();
- };
- var ok=function(blob,duration){
- This.CLog("结束录音 编码花"+(Date.now()-t1)+"ms 音频时长"+duration+"ms 文件大小"+blob.size+"b");
- if(set.takeoffEncodeChunk){//接管了输出,此时blob长度为0
- This.CLog("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据",3);
- }else if(blob.size<Math.max(100,duration/2)){//1秒小于0.5k?
- err("生成的"+set.type+"无效");
- return;
- };
- True&&True(blob,duration);
- end();
- };
- if(!This.isMock){
- var isCtxWait=This.state==3;
- if(!This.state || isCtxWait){
- err("未开始录音"+(isCtxWait?",开始录音前无用户交互导致AudioContext未运行":""));
- return;
- };
- This._stop(true);
- };
- var size=This.recSize;
- if(!size){
- err("未采集到录音");
- return;
- };
- if(!This.buffers[0]){
- err("音频buffers被释放");
- return;
- };
- if(!This[set.type]){
- err("未加载"+set.type+"编码器");
- return;
- };
-
- //环境配置检查,此处仅针对mock调用,因为open已经检查过了
- if(This.isMock){
- var checkMsg=This.envCheck(This.mockEnvInfo||{envName:"mock",canProcess:false});//没有提供环境信息的mock时没有onProcess回调
- if(checkMsg){
- err("录音错误:"+checkMsg);
- return;
- };
- };
-
- //此类型有边录边转码(Worker)支持
- var engineCtx=This.engineCtx;
- if(This[set.type+"_complete"]&&engineCtx){
- var duration=Math.round(engineCtx.pcmSize/set[sampleRateTxt]*1000);//采用后的数据长度和buffers的长度可能微小的不一致,是采样率连续转换的精度问题
-
- t1=Date.now();
- This[set.type+"_complete"](engineCtx,function(blob){
- ok(blob,duration);
- },err);
- return;
- };
-
- //标准UI线程转码,调整采样率
- t1=Date.now();
- var chunk=Recorder.SampleData(This.buffers,This[srcSampleRateTxt],set[sampleRateTxt]);
-
- set[sampleRateTxt]=chunk[sampleRateTxt];
- var res=chunk.data;
- var duration=Math.round(res.length/set[sampleRateTxt]*1000);
-
- This.CLog("采样"+size+"->"+res.length+" 花:"+(Date.now()-t1)+"ms");
-
- setTimeout(function(){
- t1=Date.now();
- This[set.type](res,function(blob){
- ok(blob,duration);
- },function(msg){
- err(msg);
- });
- });
- }
- };
- if(window[RecTxt]){
- CLog("重复引入"+RecTxt,3);
- window[RecTxt].Destroy();
- };
- window[RecTxt]=Recorder;
- //=======从WebM字节流中提取pcm数据,提取成功返回Float32Array,失败返回null||-1=====
- var WebM_Extract=function(inBytes, scope){
- 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;
-
- //先读取文件头和Track信息
- if(!scope._ht){
- readMatroskaVInt(bytes, position);//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])){
- while(pos0[0]<bytes0.length){
- var eid1=readMatroskaVInt(bytes0, pos0);
- var bytes1=readMatroskaBlock(bytes0, pos0);
- var pos1=[0],track={channels:0,sampleRate:0};
- if(BytesEq(eid1, [0xAE])){//TrackEntry
- while(pos1[0]<bytes1.length){
- var eid2=readMatroskaVInt(bytes1, pos1);
- var bytes2=readMatroskaBlock(bytes1, pos1);
- var pos2=[0];
- if(BytesEq(eid2, [0xD7])){//Track Number
- var val=BytesInt(bytes2);
- track.number=val;
- tracks[val]=track;
- }else if(BytesEq(eid2, [0x83])){//Track Type
- var val=BytesInt(bytes2);
- if(val==1) track.type="video";
- else if(val==2) {
- track.type="audio";
- if(!audioIdx) scope.track0=track;
- track.idx=audioIdx++;
- }else track.type="Type-"+val;
- }else if(BytesEq(eid2, [0x86])){//Track Codec
- var str="";
- for(var i=0;i<bytes2.length;i++){
- str+=String.fromCharCode(bytes2[i]);
- }
- track.codec=str;
- }else if(BytesEq(eid2, [0xE1])){
- while(pos2[0]<bytes2.length){//循环读取 Audio 属性
- var eid3=readMatroskaVInt(bytes2, pos2);
- var bytes3=readMatroskaBlock(bytes2, pos2);
- //采样率、位数、声道数
- if(BytesEq(eid3, [0xB5])){
- var val=0,arr=new Uint8Array(bytes3.reverse()).buffer;
- if(bytes3.length==4) val=new Float32Array(arr)[0];
- else if(bytes3.length==8) val=new Float64Array(arr)[0];
- else CLog("WebM Track !Float",1,bytes3);
- track[sampleRateTxt]=Math.round(val);
- }else if(BytesEq(eid3, [0x62,0x64])) track.bitDepth=BytesInt(bytes3);
- else if(BytesEq(eid3, [0x9F])) track.channels=BytesInt(bytes3);
- }
- }
- }
- }
- };
- scope._ht=1;
- CLog("WebM Tracks",tracks);
- endPos();
- break;
- }
- }
- }
-
- //校验音频参数信息,如果不符合代码要求,统统拒绝处理
- var track0=scope.track0;
- if(!track0)return;
- if(track0.bitDepth==16 && /FLOAT/i.test(track0.codec)){
- track0.bitDepth=32; //chrome v66 实际为浮点数
- CLog("WebM 16改32位",3);
- }
- if(track0[sampleRateTxt]!=scope[sampleRateTxt] || track0.bitDepth!=32 || track0.channels<1 || !/(\b|_)PCM\b/i.test(track0.codec)){
- scope.bytes=[];//格式非预期 无法处理,清空缓冲数据
- if(!scope.bad)CLog("WebM Track非预期",3,scope);
- scope.bad=1;
- return -1;
- }
-
- //循环读取Cluster内的SimpleBlock
- var datas=[],dataLen=0;
- while(position[0]<bytes.length){
- var eid1=readMatroskaVInt(bytes, position);
- var bytes1=readMatroskaBlock(bytes, position);
- if(!bytes1)break;//数据不全,等待缓冲
- if(BytesEq(eid1, [0xA3])){//SimpleBlock完整数据
- var trackNo=bytes1[0]&0xf;
- var track=tracks[trackNo];
- if(!track){//不可能没有,数据出错?
- CLog("WebM !Track"+trackNo,1,tracks);
- }else if(track.idx===0){
- var u8arr=new Uint8Array(bytes1.length-4);
- for(var i=4;i<bytes1.length;i++){
- u8arr[i-4]=bytes1[i];
- }
- datas.push(u8arr); dataLen+=u8arr.length;
- }
- }
- endPos();
- }
-
- if(dataLen){
- var more=new Uint8Array(bytes.length-scope.pos[0]);
- more.set(bytes.subarray(scope.pos[0]));
- scope.bytes=more; //清理已读取了的缓冲数据
- scope.pos[0]=0;
-
- var u8arr=new Uint8Array(dataLen); //已获取的音频数据
- for(var i=0,i2=0;i<datas.length;i++){
- u8arr.set(datas[i],i2);
- i2+=datas[i].length;
- }
- var arr=new Float32Array(u8arr.buffer);
-
- if(track0.channels>1){//多声道,提取一个声道
- var arr2=[];
- for(var i=0;i<arr.length;){
- arr2.push(arr[i]);
- i+=track0.channels;
- }
- arr=new Float32Array(arr2);
- };
- return arr;
- }
- };
- //两个字节数组内容是否相同
- 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读取=====
- //流量统计用1像素图片地址,设置为空将不参与统计
- Recorder.TrafficImgUrl="//ia.51.la/go1?id=20469973&pvFlag=1";
- var Traffic=Recorder.Traffic=function(report){
- report=report?"/"+RecTxt+"/Report/"+report:"";
- var imgUrl=Recorder.TrafficImgUrl;
- if(imgUrl){
- var data=Recorder.Traffic;
- var m=/^(https?:..[^\/#]*\/?)[^#]*/i.exec(location.href)||[];
- var host=(m[1]||"http://file/");
- var idf=(m[0]||host)+report;
-
- if(imgUrl.indexOf("//")==0){
- //给url加上http前缀,如果是file协议下,不加前缀没法用
- if(/^https:/i.test(idf)){
- imgUrl="https:"+imgUrl;
- }else{
- imgUrl="http:"+imgUrl;
- };
- };
- if(report){
- imgUrl=imgUrl+"&cu="+encodeURIComponent(host+report);
- };
-
- if(!data[idf]){
- data[idf]=1;
-
- var img=new Image();
- img.src=imgUrl;
- CLog("Traffic Analysis Image: "+(report||RecTxt+".TrafficImgUrl="+Recorder.TrafficImgUrl));
- };
- };
- };
- }));
|