/* 录音 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 LM="2021-08-03 20:01:03"; var NOOP=function(){}; //end 兼容环境 ****从以下开始copy源码***** var Recorder=function(set){ return new initFn(set); }; //是否已经打开了全局的麦克风录音,所有工作都已经准备好了,就等接收音频数据了 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("Recorder Destroy"); Disconnect();//断开可能存在的全局Stream、资源 for(var k in DestroyList){ DestroyList[k](); }; }; var DestroyList={}; //登记一个需要销毁全局资源的处理方法 Recorder.BindDestroy=function(key,call){ DestroyList[key]=call; }; //判断浏览器是否支持录音,随时可以调用。注意:仅仅是检测浏览器支持情况,不会判断和调起用户授权,不会判断是否支持特定格式录音。 Recorder.Support=function(){ var AC=window.AudioContext; if(!AC){ AC=window.webkitAudioContext; }; if(!AC){ return false; }; var scope=navigator.mediaDevices||{}; if(!scope.getUserMedia){ scope=navigator; scope.getUserMedia||(scope.getUserMedia=scope.webkitGetUserMedia||scope.mozGetUserMedia||scope.msGetUserMedia); }; if(!scope.getUserMedia){ return false; }; Recorder.Scope=scope; 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 true; }; /*初始化H5音频采集连接。如果自行提供了sourceStream将只进行一次简单的连接处理。如果是普通麦克风录音,此时的Stream是全局的,Safari上断开后就无法再次进行连接使用,表现为静音,因此使用全部使用全局处理避免调用到disconnect;全局处理也有利于屏蔽底层细节,start时无需再调用底层接口,提升兼容、可靠性。*/ var Connect=function(streamStore){ streamStore=streamStore||Recorder; var bufferSize=streamStore.BufferSize||Recorder.BufferSize; var ctx=Recorder.Ctx,stream=streamStore.Stream; var media=stream._m=ctx.createMediaStreamSource(stream); var process=stream._p=(ctx.createScriptProcessor||ctx.createJavaScriptNode).call(ctx,bufferSize,1,1);//单声道,省的数据处理复杂 media.connect(process); process.connect(ctx.destination); var calls=stream._call; process.onaudioprocess=function(e){ for(var k0 in calls){//has item var o=e.inputBuffer.getChannelData(0);//块是共享的,必须复制出来 var size=o.length; var pcm=new Int16Array(size); var sum=0; for(var j=0;j=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 size=0; for(var i=index;i1){//新采样低于录音采样,进行抽样 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;i0){ 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; }; //带时间的日志输出,CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn,否则当做内容输出,第一个参数不能是对象因为要拼接时间,后面可以接无数个输出参数 var 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 arr=["["+t+" Recorder]"+msg]; var a=arguments; var i=2,fn=console.log; if(typeof(err)=="number"){ fn=err==1?console.error:err==3?console.warn:fn; }else{ i=1; }; for(;i3000){ 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; 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.sampleRate,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.sampleRate; }; 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"+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.Recorder){ window.Recorder.Destroy(); }; window.Recorder=Recorder; //end ****copy源码结束***** Recorder.LM=LM; //流量统计用1像素图片地址,设置为空将不参与统计 Recorder.TrafficImgUrl="//ia.51.la/go1?id=20469973&pvFlag=1"; Recorder.Traffic=function(){ var imgUrl=Recorder.TrafficImgUrl; if(imgUrl){ var data=Recorder.Traffic; var idf=location.href.replace(/#.*/,""); if(imgUrl.indexOf("//")==0){ //给url加上http前缀,如果是file协议下,不加前缀没法用 if(/^https:/i.test(idf)){ imgUrl="https:"+imgUrl; }else{ imgUrl="http:"+imgUrl; }; }; if(!data[idf]){ data[idf]=1; var img=new Image(); img.src=imgUrl; CLog("Traffic Analysis Image: Recorder.TrafficImgUrl="+Recorder.TrafficImgUrl); }; }; }; }));