recorder-core.js 51 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493
  1. /*
  2. 录音
  3. https://github.com/xiangyuecn/Recorder
  4. */
  5. (function(factory){
  6. factory(window);
  7. //umd returnExports.js
  8. if(typeof(define)=='function' && define.amd){
  9. define(function(){
  10. return Recorder;
  11. });
  12. };
  13. if(typeof(module)=='object' && module.exports){
  14. module.exports=Recorder;
  15. };
  16. }(function(window){
  17. "use strict";
  18. var NOOP=function(){};
  19. var Recorder=function(set){
  20. return new initFn(set);
  21. };
  22. Recorder.LM="2023-02-01 18:05";
  23. var RecTxt="Recorder";
  24. var getUserMediaTxt="getUserMedia";
  25. var srcSampleRateTxt="srcSampleRate";
  26. var sampleRateTxt="sampleRate";
  27. var CatchTxt="catch";
  28. //是否已经打开了全局的麦克风录音,所有工作都已经准备好了,就等接收音频数据了
  29. Recorder.IsOpen=function(){
  30. var stream=Recorder.Stream;
  31. if(stream){
  32. var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
  33. var track=tracks[0];
  34. if(track){
  35. var state=track.readyState;
  36. return state=="live"||state==track.LIVE;
  37. };
  38. };
  39. return false;
  40. };
  41. /*H5录音时的AudioContext缓冲大小。会影响H5录音时的onProcess调用速率,相对于AudioContext.sampleRate=48000时,4096接近12帧/s,调节此参数可生成比较流畅的回调动画。
  42. 取值256, 512, 1024, 2048, 4096, 8192, or 16384
  43. 注意,取值不能过低,2048开始不同浏览器可能回调速率跟不上造成音质问题。
  44. 一般无需调整,调整后需要先close掉已打开的录音,再open时才会生效。
  45. */
  46. Recorder.BufferSize=4096;
  47. //销毁已持有的所有全局资源,当要彻底移除Recorder时需要显式的调用此方法
  48. Recorder.Destroy=function(){
  49. CLog(RecTxt+" Destroy");
  50. Disconnect();//断开可能存在的全局Stream、资源
  51. for(var k in DestroyList){
  52. DestroyList[k]();
  53. };
  54. };
  55. var DestroyList={};
  56. //登记一个需要销毁全局资源的处理方法
  57. Recorder.BindDestroy=function(key,call){
  58. DestroyList[key]=call;
  59. };
  60. //判断浏览器是否支持录音,随时可以调用。注意:仅仅是检测浏览器支持情况,不会判断和调起用户授权,不会判断是否支持特定格式录音。
  61. Recorder.Support=function(){
  62. var scope=navigator.mediaDevices||{};
  63. if(!scope[getUserMediaTxt]){
  64. scope=navigator;
  65. scope[getUserMediaTxt]||(scope[getUserMediaTxt]=scope.webkitGetUserMedia||scope.mozGetUserMedia||scope.msGetUserMedia);
  66. };
  67. if(!scope[getUserMediaTxt]){
  68. return false;
  69. };
  70. Recorder.Scope=scope;
  71. if(!Recorder.GetContext()){
  72. return false;
  73. };
  74. return true;
  75. };
  76. //获取全局的AudioContext对象,如果浏览器不支持将返回null
  77. Recorder.GetContext=function(){
  78. var AC=window.AudioContext;
  79. if(!AC){
  80. AC=window.webkitAudioContext;
  81. };
  82. if(!AC){
  83. return null;
  84. };
  85. if(!Recorder.Ctx||Recorder.Ctx.state=="closed"){
  86. //不能反复构造,低版本number of hardware contexts reached maximum (6)
  87. Recorder.Ctx=new AC();
  88. Recorder.BindDestroy("Ctx",function(){
  89. var ctx=Recorder.Ctx;
  90. if(ctx&&ctx.close){//能关掉就关掉,关不掉就保留着
  91. ctx.close();
  92. Recorder.Ctx=0;
  93. };
  94. });
  95. };
  96. return Recorder.Ctx;
  97. };
  98. /*是否启用MediaRecorder.WebM.PCM来进行音频采集连接(如果浏览器支持的话),默认启用,禁用或者不支持时将使用AudioWorklet或ScriptProcessor来连接;MediaRecorder采集到的音频数据比其他方式更好,几乎不存在丢帧现象,所以音质明显会好很多,建议保持开启*/
  99. var ConnectEnableWebM="ConnectEnableWebM";
  100. Recorder[ConnectEnableWebM]=true;
  101. /*是否启用AudioWorklet特性来进行音频采集连接(如果浏览器支持的话),默认禁用,禁用或不支持时将使用过时的ScriptProcessor来连接(如果方法还在的话),当前AudioWorklet的实现在移动端没有ScriptProcessor稳健;ConnectEnableWebM如果启用并且有效时,本参数将不起作用*/
  102. var ConnectEnableWorklet="ConnectEnableWorklet";
  103. Recorder[ConnectEnableWorklet]=false;
  104. /*初始化H5音频采集连接。如果自行提供了sourceStream将只进行一次简单的连接处理。如果是普通麦克风录音,此时的Stream是全局的,Safari上断开后就无法再次进行连接使用,表现为静音,因此使用全部使用全局处理避免调用到disconnect;全局处理也有利于屏蔽底层细节,start时无需再调用底层接口,提升兼容、可靠性。*/
  105. var Connect=function(streamStore,isUserMedia){
  106. var bufferSize=streamStore.BufferSize||Recorder.BufferSize;
  107. var ctx=Recorder.Ctx,stream=streamStore.Stream;
  108. var mediaConn=function(node){
  109. var media=stream._m=ctx.createMediaStreamSource(stream);
  110. var ctxDest=ctx.destination,cmsdTxt="createMediaStreamDestination";
  111. if(ctx[cmsdTxt]){
  112. ctxDest=ctx[cmsdTxt]();
  113. };
  114. media.connect(node);
  115. node.connect(ctxDest);
  116. }
  117. var isWebM,isWorklet,badInt,webMTips="";
  118. var calls=stream._call;
  119. //浏览器回传的音频数据处理
  120. var onReceive=function(float32Arr){
  121. for(var k0 in calls){//has item
  122. var size=float32Arr.length;
  123. var pcm=new Int16Array(size);
  124. var sum=0;
  125. for(var j=0;j<size;j++){//floatTo16BitPCM
  126. var s=Math.max(-1,Math.min(1,float32Arr[j]));
  127. s=s<0?s*0x8000:s*0x7FFF;
  128. pcm[j]=s;
  129. sum+=Math.abs(s);
  130. };
  131. for(var k in calls){
  132. calls[k](pcm,sum);
  133. };
  134. return;
  135. };
  136. };
  137. var scriptProcessor="ScriptProcessor";//一堆字符串名字,有利于压缩js
  138. var audioWorklet="audioWorklet";
  139. var recAudioWorklet=RecTxt+" "+audioWorklet;
  140. var RecProc="RecProc";
  141. var MediaRecorderTxt="MediaRecorder";
  142. var MRWebMPCM=MediaRecorderTxt+".WebM.PCM";
  143. //===================连接方式三=========================
  144. //古董级别的 ScriptProcessor 处理,目前所有浏览器均兼容,虽然是过时的方法,但更稳健,移动端性能比AudioWorklet强
  145. var oldFn=ctx.createScriptProcessor||ctx.createJavaScriptNode;
  146. var oldIsBest="。由于"+audioWorklet+"内部1秒375次回调,在移动端可能会有性能问题导致回调丢失录音变短,PC端无影响,暂不建议开启"+audioWorklet+"。";
  147. var oldScript=function(){
  148. isWorklet=stream.isWorklet=false;
  149. _Disconn_n(stream);
  150. CLog("Connect采用老的"+scriptProcessor+","+(Recorder[ConnectEnableWorklet]?"但已":"可")+"设置"+RecTxt+"."+ConnectEnableWorklet+"=true尝试启用"+audioWorklet+webMTips+oldIsBest,3);
  151. var process=stream._p=oldFn.call(ctx,bufferSize,1,1);//单声道,省的数据处理复杂
  152. mediaConn(process);
  153. var _DsetTxt="_D220626",_Dset=Recorder[_DsetTxt];if(_Dset)CLog("Use "+RecTxt+"."+_DsetTxt,3);
  154. process.onaudioprocess=function(e){
  155. var arr=e.inputBuffer.getChannelData(0);
  156. if(_Dset){//临时调试用的参数,未来会被删除
  157. arr=new Float32Array(arr);//块是共享的,必须复制出来
  158. setTimeout(function(){ onReceive(arr) });//立即退出回调,试图减少对浏览器录音的影响
  159. }else{
  160. onReceive(arr);
  161. };
  162. };
  163. };
  164. //===================连接方式二=========================
  165. var connWorklet=function(){
  166. //尝试开启AudioWorklet处理
  167. isWebM=stream.isWebM=false;
  168. _Disconn_r(stream);
  169. isWorklet=stream.isWorklet=!oldFn || Recorder[ConnectEnableWorklet];
  170. var AwNode=window.AudioWorkletNode;
  171. if(!(isWorklet && ctx[audioWorklet] && AwNode)){
  172. oldScript();//被禁用 或 不支持,直接使用老的
  173. return;
  174. };
  175. var clazzUrl=function(){
  176. var xf=function(f){return f.toString().replace(/^function|DEL_/g,"").replace(/\$RA/g,recAudioWorklet)};
  177. var clazz='class '+RecProc+' extends AudioWorkletProcessor{';
  178. clazz+="constructor "+xf(function(option){
  179. DEL_super(option);
  180. var This=this,bufferSize=option.processorOptions.bufferSize;
  181. This.bufferSize=bufferSize;
  182. This.buffer=new Float32Array(bufferSize*2);//乱给size搞乱缓冲区不管
  183. This.pos=0;
  184. This.port.onmessage=function(e){
  185. if(e.data.kill){
  186. This.kill=true;
  187. console.log("$RA kill call");
  188. }
  189. };
  190. console.log("$RA .ctor call", option);
  191. });
  192. //https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletProcessor/process 每次回调128个采样数据,1秒375次回调,高频导致移动端性能问题,结果就是回调次数缺斤少两,进而导致丢失数据,PC端似乎没有性能问题
  193. clazz+="process "+xf(function(input,b,c){//需要等到ctx激活后才会有回调
  194. var This=this,bufferSize=This.bufferSize;
  195. var buffer=This.buffer,pos=This.pos;
  196. input=(input[0]||[])[0]||[];
  197. if(input.length){
  198. buffer.set(input,pos);
  199. pos+=input.length;
  200. var len=~~(pos/bufferSize)*bufferSize;
  201. if(len){
  202. this.port.postMessage({ val: buffer.slice(0,len) });
  203. var more=buffer.subarray(len,pos);
  204. buffer=new Float32Array(bufferSize*2);
  205. buffer.set(more);
  206. pos=more.length;
  207. This.buffer=buffer;
  208. }
  209. This.pos=pos;
  210. }
  211. return !This.kill;
  212. });
  213. clazz+='}'
  214. +'try{'
  215. +'registerProcessor("'+RecProc+'", '+RecProc+')'
  216. +'}catch(e){'
  217. +'console.error("'+recAudioWorklet+'注册失败",e)'
  218. +'}';
  219. //URL.createObjectURL 本地有些浏览器会报 Not allowed to load local resource,直接用dataurl
  220. return "data:text/javascript;base64,"+btoa(unescape(encodeURIComponent(clazz)));
  221. };
  222. var awNext=function(){//可以继续,没有调用断开
  223. return isWorklet && stream._na;
  224. };
  225. var nodeAlive=stream._na=function(){
  226. //start时会调用,只要没有收到数据就断定AudioWorklet有问题,恢复用老的
  227. if(badInt!==""){//没有回调过数据
  228. clearTimeout(badInt);
  229. badInt=setTimeout(function(){
  230. badInt=0;
  231. if(awNext()){
  232. CLog(audioWorklet+"未返回任何音频,恢复使用"+scriptProcessor,3);
  233. oldFn&&oldScript();//未来没有老的,可能是误判
  234. };
  235. },500);
  236. };
  237. };
  238. var createNode=function(){
  239. if(!awNext())return;
  240. var node=stream._n=new AwNode(ctx, RecProc, {
  241. processorOptions:{bufferSize:bufferSize}
  242. });
  243. mediaConn(node);
  244. node.port.onmessage=function(e){
  245. if(badInt){
  246. clearTimeout(badInt);badInt="";
  247. };
  248. if(awNext()){
  249. onReceive(e.data.val);
  250. }else if(!isWorklet){
  251. CLog(audioWorklet+"多余回调",3);
  252. };
  253. };
  254. CLog("Connect采用"+audioWorklet+",设置"+RecTxt+"."+ConnectEnableWorklet+"=false可恢复老式"+scriptProcessor+webMTips+oldIsBest,3);
  255. };
  256. //如果start时的resume和下面的构造node同时进行,将会导致部分浏览器崩溃,源码assets中 ztest_chrome_bug_AudioWorkletNode.html 可测试。所以,将所有代码套到resume里面(不管catch),避免出现这个问题
  257. ctx.resume()[calls&&"finally"](function(){//注释掉这行 观摩浏览器崩溃 STATUS_ACCESS_VIOLATION
  258. if(!awNext())return;
  259. if(ctx[RecProc]){
  260. createNode();
  261. return;
  262. };
  263. var url=clazzUrl();
  264. ctx[audioWorklet].addModule(url).then(function(e){
  265. if(!awNext())return;
  266. ctx[RecProc]=1;
  267. createNode();
  268. if(badInt){//重新计时
  269. nodeAlive();
  270. };
  271. })[CatchTxt](function(e){ //fix 关键字,保证catch压缩时保持字符串形式
  272. CLog(audioWorklet+".addModule失败",1,e);
  273. awNext()&&oldScript();
  274. });
  275. });
  276. };
  277. //===================连接方式一=========================
  278. var connWebM=function(){
  279. //尝试开启MediaRecorder录制webm+pcm处理
  280. var MR=window[MediaRecorderTxt];
  281. var onData="ondataavailable";
  282. var webmType="audio/webm; codecs=pcm";
  283. isWebM=stream.isWebM=Recorder[ConnectEnableWebM];
  284. var supportMR=MR && (onData in MR.prototype) && MR.isTypeSupported(webmType);
  285. webMTips=supportMR?"":"(此浏览器不支持"+MRWebMPCM+")";
  286. if(!isUserMedia || !isWebM || !supportMR){
  287. connWorklet(); //非麦克风录音(MediaRecorder采样率不可控) 或 被禁用 或 不支持MediaRecorder 或 不支持webm+pcm
  288. return;
  289. }
  290. var mrNext=function(){//可以继续,没有调用断开
  291. return isWebM && stream._ra;
  292. };
  293. var mrAlive=stream._ra=function(){
  294. //start时会调用,只要没有收到数据就断定MediaRecorder有问题,降级处理
  295. if(badInt!==""){//没有回调过数据
  296. clearTimeout(badInt);
  297. badInt=setTimeout(function(){
  298. //badInt=0; 保留给nodeAlive继续判断
  299. if(mrNext()){
  300. CLog(MediaRecorderTxt+"未返回任何音频,降级使用"+audioWorklet,3);
  301. connWorklet();
  302. };
  303. },500);
  304. };
  305. };
  306. var mrSet=Object.assign({mimeType:webmType}, Recorder.ConnectWebMOptions);
  307. var mr=stream._r=new MR(stream, mrSet);
  308. var webmData=stream._rd={sampleRate:ctx[sampleRateTxt]};
  309. mr[onData]=function(e){
  310. //提取webm中的pcm数据,提取失败就等着badInt超时降级处理
  311. var reader=new FileReader();
  312. reader.onloadend=function(){
  313. if(mrNext()){
  314. var f32arr=WebM_Extract(new Uint8Array(reader.result),webmData);
  315. if(!f32arr)return;
  316. if(f32arr==-1){//无法提取,立即降级
  317. connWorklet();
  318. return;
  319. };
  320. if(badInt){
  321. clearTimeout(badInt);badInt="";
  322. };
  323. onReceive(f32arr);
  324. }else if(!isWebM){
  325. CLog(MediaRecorderTxt+"多余回调",3);
  326. };
  327. };
  328. reader.readAsArrayBuffer(e.data);
  329. };
  330. mr.start(~~(bufferSize/48));//按48k时的回调间隔
  331. CLog("Connect采用"+MRWebMPCM+",设置"+RecTxt+"."+ConnectEnableWebM+"=false可恢复使用"+audioWorklet+"或老式"+scriptProcessor);
  332. };
  333. connWebM();
  334. };
  335. var ConnAlive=function(stream){
  336. if(stream._na) stream._na(); //检查AudioWorklet连接是否有效,无效就回滚到老的ScriptProcessor
  337. if(stream._ra) stream._ra(); //检查MediaRecorder连接是否有效,无效就降级处理
  338. };
  339. var _Disconn_n=function(stream){
  340. stream._na=null;
  341. if(stream._n){
  342. stream._n.port.postMessage({kill:true});
  343. stream._n.disconnect();
  344. stream._n=null;
  345. };
  346. };
  347. var _Disconn_r=function(stream){
  348. stream._ra=null;
  349. if(stream._r){
  350. stream._r.stop();
  351. stream._r=null;
  352. };
  353. };
  354. var Disconnect=function(streamStore){
  355. streamStore=streamStore||Recorder;
  356. var isGlobal=streamStore==Recorder;
  357. var stream=streamStore.Stream;
  358. if(stream){
  359. if(stream._m){
  360. stream._m.disconnect();
  361. stream._m=null;
  362. };
  363. if(stream._p){
  364. stream._p.disconnect();
  365. stream._p.onaudioprocess=stream._p=null;
  366. };
  367. _Disconn_n(stream);
  368. _Disconn_r(stream);
  369. if(isGlobal){//全局的时候,要把流关掉(麦克风),直接提供的流不处理
  370. var tracks=stream.getTracks&&stream.getTracks()||stream.audioTracks||[];
  371. for(var i=0;i<tracks.length;i++){
  372. var track=tracks[i];
  373. track.stop&&track.stop();
  374. };
  375. stream.stop&&stream.stop();
  376. };
  377. };
  378. streamStore.Stream=0;
  379. };
  380. /*对pcm数据的采样率进行转换
  381. pcmDatas: [[Int16,...]] pcm片段列表
  382. pcmSampleRate:48000 pcm数据的采样率
  383. newSampleRate:16000 需要转换成的采样率,newSampleRate>=pcmSampleRate时不会进行任何处理,小于时会进行重新采样
  384. prevChunkInfo:{} 可选,上次调用时的返回值,用于连续转换,本次调用将从上次结束位置开始进行处理。或可自行定义一个ChunkInfo从pcmDatas指定的位置开始进行转换
  385. option:{ 可选,配置项
  386. frameSize:123456 帧大小,每帧的PCM Int16的数量,采样率转换后的pcm长度为frameSize的整数倍,用于连续转换。目前仅在mp3格式时才有用,frameSize取值为1152,这样编码出来的mp3时长和pcm的时长完全一致,否则会因为mp3最后一帧录音不够填满时添加填充数据导致mp3的时长变长。
  387. frameType:"" 帧类型,一般为rec.set.type,提供此参数时无需提供frameSize,会自动使用最佳的值给frameSize赋值,目前仅支持mp3=1152(MPEG1 Layer3的每帧采采样数),其他类型=1。
  388. 以上两个参数用于连续转换时使用,最多使用一个,不提供时不进行帧的特殊处理,提供时必须同时提供prevChunkInfo才有作用。最后一段数据处理时无需提供帧大小以便输出最后一丁点残留数据。
  389. }
  390. 返回ChunkInfo:{
  391. //可定义,从指定位置开始转换到结尾
  392. index:0 pcmDatas已处理到的索引
  393. offset:0.0 已处理到的index对应的pcm中的偏移的下一个位置
  394. //仅作为返回值
  395. frameNext:null||[Int16,...] 下一帧的部分数据,frameSize设置了的时候才可能会有
  396. sampleRate:16000 结果的采样率,<=newSampleRate
  397. data:[Int16,...] 转换后的PCM结果;如果是连续转换,并且pcmDatas中并没有新数据时,data的长度可能为0
  398. }
  399. */
  400. Recorder.SampleData=function(pcmDatas,pcmSampleRate,newSampleRate,prevChunkInfo,option){
  401. prevChunkInfo||(prevChunkInfo={});
  402. var index=prevChunkInfo.index||0;
  403. var offset=prevChunkInfo.offset||0;
  404. var frameNext=prevChunkInfo.frameNext||[];
  405. option||(option={});
  406. var frameSize=option.frameSize||1;
  407. if(option.frameType){
  408. frameSize=option.frameType=="mp3"?1152:1;
  409. };
  410. var nLen=pcmDatas.length;
  411. if(index>nLen+1){
  412. CLog("SampleData似乎传入了未重置chunk "+index+">"+nLen,3);
  413. };
  414. var size=0;
  415. for(var i=index;i<nLen;i++){
  416. size+=pcmDatas[i].length;
  417. };
  418. size=Math.max(0,size-Math.floor(offset));
  419. //采样 https://www.cnblogs.com/blqw/p/3782420.html
  420. var step=pcmSampleRate/newSampleRate;
  421. if(step>1){//新采样低于录音采样,进行抽样
  422. size=Math.floor(size/step);
  423. }else{//新采样高于录音采样不处理,省去了插值处理
  424. step=1;
  425. newSampleRate=pcmSampleRate;
  426. };
  427. size+=frameNext.length;
  428. var res=new Int16Array(size);
  429. var idx=0;
  430. //添加上一次不够一帧的剩余数据
  431. for(var i=0;i<frameNext.length;i++){
  432. res[idx]=frameNext[i];
  433. idx++;
  434. };
  435. //处理数据
  436. for (;index<nLen;index++) {
  437. var o=pcmDatas[index];
  438. var i=offset,il=o.length;
  439. while(i<il){
  440. //res[idx]=o[Math.round(i)]; 直接简单抽样
  441. //https://www.cnblogs.com/xiaoqi/p/6993912.html
  442. //当前点=当前点+到后面一个点之间的增量,音质比直接简单抽样好些
  443. var before = Math.floor(i);
  444. var after = Math.ceil(i);
  445. var atPoint = i - before;
  446. var beforeVal=o[before];
  447. var afterVal=after<il ? o[after]
  448. : (//后个点越界了,查找下一个数组
  449. (pcmDatas[index+1]||[beforeVal])[0]||0
  450. );
  451. res[idx]=beforeVal+(afterVal-beforeVal)*atPoint;
  452. idx++;
  453. i+=step;//抽样
  454. };
  455. offset=i-il;
  456. };
  457. //帧处理
  458. frameNext=null;
  459. var frameNextSize=res.length%frameSize;
  460. if(frameNextSize>0){
  461. var u8Pos=(res.length-frameNextSize)*2;
  462. frameNext=new Int16Array(res.buffer.slice(u8Pos));
  463. res=new Int16Array(res.buffer.slice(0,u8Pos));
  464. };
  465. return {
  466. index:index
  467. ,offset:offset
  468. ,frameNext:frameNext
  469. ,sampleRate:newSampleRate
  470. ,data:res
  471. };
  472. };
  473. /*计算音量百分比的一个方法
  474. pcmAbsSum: pcm Int16所有采样的绝对值的和
  475. pcmLength: pcm长度
  476. 返回值:0-100,主要当做百分比用
  477. 注意:这个不是分贝,因此没用volume当做名称*/
  478. Recorder.PowerLevel=function(pcmAbsSum,pcmLength){
  479. /*计算音量 https://blog.csdn.net/jody1989/article/details/73480259
  480. 更高灵敏度算法:
  481. 限定最大感应值10000
  482. 线性曲线:低音量不友好
  483. power/10000*100
  484. 对数曲线:低音量友好,但需限定最低感应值
  485. (1+Math.log10(power/10000))*100
  486. */
  487. var power=(pcmAbsSum/pcmLength) || 0;//NaN
  488. var level;
  489. if(power<1251){//1250的结果10%,更小的音量采用线性取值
  490. level=Math.round(power/1250*10);
  491. }else{
  492. level=Math.round(Math.min(100,Math.max(0,(1+Math.log(power/10000)/Math.log(10))*100)));
  493. };
  494. return level;
  495. };
  496. /*计算音量,单位dBFS(满刻度相对电平)
  497. maxSample: 为16位pcm采样的绝对值中最大的一个(计算峰值音量),或者为pcm中所有采样的绝对值的平局值
  498. 返回值:-100~0 (最大值0dB,最小值-100代替-∞)
  499. */
  500. Recorder.PowerDBFS=function(maxSample){
  501. var val=Math.max(0.1, maxSample||0),Pref=0x7FFF;
  502. val=Math.min(val,Pref);
  503. //https://www.logiclocmusic.com/can-you-tell-the-decibel/
  504. //https://blog.csdn.net/qq_17256689/article/details/120442510
  505. val=20*Math.log(val/Pref)/Math.log(10);
  506. return Math.max(-100,Math.round(val));
  507. };
  508. //带时间的日志输出,可设为一个空函数来屏蔽日志输出
  509. //CLog(msg,errOrLogMsg, logMsg...) err为数字时代表日志类型1:error 2:log默认 3:warn,否则当做内容输出,第一个参数不能是对象因为要拼接时间,后面可以接无数个输出参数
  510. Recorder.CLog=function(msg,err){
  511. var now=new Date();
  512. var t=("0"+now.getMinutes()).substr(-2)
  513. +":"+("0"+now.getSeconds()).substr(-2)
  514. +"."+("00"+now.getMilliseconds()).substr(-3);
  515. var recID=this&&this.envIn&&this.envCheck&&this.id;
  516. var arr=["["+t+" "+RecTxt+(recID?":"+recID:"")+"]"+msg];
  517. var a=arguments,console=window.console||{};
  518. var i=2,fn=console.log;
  519. if(typeof(err)=="number"){
  520. fn=err==1?console.error:err==3?console.warn:fn;
  521. }else{
  522. i=1;
  523. };
  524. for(;i<a.length;i++){
  525. arr.push(a[i]);
  526. };
  527. if(IsLoser){//古董浏览器,仅保证基本的可执行不代码异常
  528. fn&&fn("[IsLoser]"+arr[0],arr.length>1?arr:"");
  529. }else{
  530. fn.apply(console,arr);
  531. };
  532. };
  533. var CLog=function(){ Recorder.CLog.apply(this,arguments); };
  534. var IsLoser=true;try{IsLoser=!console.log.apply;}catch(e){};
  535. var ID=0;
  536. function initFn(set){
  537. this.id=++ID;
  538. //如果开启了流量统计,这里将发送一个图片请求
  539. Traffic();
  540. var o={
  541. type:"mp3" //输出类型:mp3,wav,wav输出文件尺寸超大不推荐使用,但mp3编码支持会导致js文件超大,如果不需支持mp3可以使js文件大幅减小
  542. ,bitRate:16 //比特率 wav:16或8位,MP3:8kbps 1k/s,8kbps 2k/s 录音文件很小
  543. ,sampleRate:16000 //采样率,wav格式大小=sampleRate*时间;mp3此项对低比特率有影响,高比特率几乎无影响。
  544. //wav任意值,mp3取值范围:48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000
  545. //采样率参考https://www.cnblogs.com/devin87/p/mp3-recorder.html
  546. ,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中本次回调的结尾位置。
  547. //*******高级设置******
  548. //,sourceStream:MediaStream Object
  549. //可选直接提供一个媒体流,从这个流中录制、实时处理音频数据(当前Recorder实例独享此流);不提供时为普通的麦克风录音,由getUserMedia提供音频流(所有Recorder实例共享同一个流)
  550. //比如:audio、video标签dom节点的captureStream方法(实验特性,不同浏览器支持程度不高)返回的流;WebRTC中的remote流;自己创建的流等
  551. //注意:流内必须至少存在一条音轨(Audio Track),比如audio标签必须等待到可以开始播放后才会有音轨,否则open会失败
  552. //,audioTrackSet:{ deviceId:"",groupId:"", autoGainControl:true, echoCancellation:true, noiseSuppression:true }
  553. //普通麦克风录音时getUserMedia方法的audio配置参数,比如指定设备id,回声消除、降噪开关;注意:提供的任何配置值都不一定会生效
  554. //由于麦克风是全局共享的,所以新配置后需要close掉以前的再重新open
  555. //更多参考: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
  556. //,disableEnvInFix:false 内部参数,禁用设备卡顿时音频输入丢失补偿功能
  557. //,takeoffEncodeChunk:NOOP //fn(chunkBytes) chunkBytes=[Uint8,...]:实时编码环境下接管编码器输出,当编码器实时编码出一块有效的二进制音频数据时实时回调此方法;参数为二进制的Uint8Array,就是编码出来的音频数据片段,所有的chunkBytes拼接在一起即为完整音频。本实现的想法最初由QQ2543775048提出
  558. //当提供此回调方法时,将接管编码器的数据输出,编码器内部将放弃存储生成的音频数据;环境要求比较苛刻:如果当前环境不支持实时编码处理,将在open时直接走fail逻辑
  559. //因此提供此回调后调用stop方法将无法获得有效的音频数据,因为编码器内没有音频数据,因此stop时返回的blob将是一个字节长度为0的blob
  560. //目前只有mp3格式实现了实时编码,在支持实时处理的环境中将会实时的将编码出来的mp3片段通过此方法回调,所有的chunkBytes拼接到一起即为完整的mp3,此种拼接的结果比mock方法实时生成的音质更加,因为天然避免了首尾的静默
  561. //目前除mp3外其他格式不可以提供此回调,提供了将在open时直接走fail逻辑
  562. };
  563. for(var k in set){
  564. o[k]=set[k];
  565. };
  566. this.set=o;
  567. this._S=9;//stop同步锁,stop可以阻止open过程中还未运行的start
  568. this.Sync={O:9,C:9};//和Recorder.Sync一致,只不过这个是非全局的,仅用来简化代码逻辑,无实际作用
  569. };
  570. //同步锁,控制对Stream的竞争;用于close时中断异步的open;一个对象open如果变化了都要阻止close,Stream的控制权交个新的对象
  571. Recorder.Sync={/*open*/O:9,/*close*/C:9};
  572. Recorder.prototype=initFn.prototype={
  573. CLog:CLog
  574. //流相关的数据存储在哪个对象里面;如果提供了sourceStream,数据直接存储在当前对象中,否则存储在全局
  575. ,_streamStore:function(){
  576. if(this.set.sourceStream){
  577. return this;
  578. }else{
  579. return Recorder;
  580. }
  581. }
  582. //打开录音资源True(),False(msg,isUserNotAllow),需要调用close。注意:此方法是异步的;一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音
  583. ,open:function(True,False){
  584. var This=this,streamStore=This._streamStore();
  585. True=True||NOOP;
  586. var failCall=function(errMsg,isUserNotAllow){
  587. isUserNotAllow=!!isUserNotAllow;
  588. This.CLog("录音open失败:"+errMsg+",isUserNotAllow:"+isUserNotAllow,1);
  589. False&&False(errMsg,isUserNotAllow);
  590. };
  591. var ok=function(){
  592. This.CLog("open ok id:"+This.id);
  593. True();
  594. This._SO=0;//解除stop对open中的start调用的阻止
  595. };
  596. //同步锁
  597. var Lock=streamStore.Sync;
  598. var lockOpen=++Lock.O,lockClose=Lock.C;
  599. This._O=This._O_=lockOpen;//记住当前的open,如果变化了要阻止close,这里假定了新对象已取代当前对象并且不再使用
  600. This._SO=This._S;//记住open过程中的stop,中途任何stop调用后都不能继续open中的start
  601. var lockFail=function(){
  602. //允许多次open,但不允许任何一次close,或者自身已经调用了关闭
  603. if(lockClose!=Lock.C || !This._O){
  604. var err="open被取消";
  605. if(lockOpen==Lock.O){
  606. //无新的open,已经调用了close进行取消,此处应让上次的close明确生效
  607. This.close();
  608. }else{
  609. err="open被中断";
  610. };
  611. failCall(err);
  612. return true;
  613. };
  614. };
  615. //环境配置检查
  616. var checkMsg=This.envCheck({envName:"H5",canProcess:true});
  617. if(checkMsg){
  618. failCall("不能录音:"+checkMsg);
  619. return;
  620. };
  621. //***********已直接提供了音频流************
  622. if(This.set.sourceStream){
  623. if(!Recorder.GetContext()){
  624. failCall("不支持此浏览器从流中获取录音");
  625. return;
  626. };
  627. Disconnect(streamStore);//可能已open过,直接先尝试断开
  628. This.Stream=This.set.sourceStream;
  629. This.Stream._call={};
  630. try{
  631. Connect(streamStore);
  632. }catch(e){
  633. failCall("从流中打开录音失败:"+e.message);
  634. return;
  635. }
  636. ok();
  637. return;
  638. };
  639. //***********打开麦克风得到全局的音频流************
  640. var codeFail=function(code,msg){
  641. try{//跨域的优先检测一下
  642. window.top.a;
  643. }catch(e){
  644. failCall('无权录音(跨域,请尝试给iframe添加麦克风访问策略,如allow="camera;microphone")');
  645. return;
  646. };
  647. if(/Permission|Allow/i.test(code)){
  648. failCall("用户拒绝了录音权限",true);
  649. }else if(window.isSecureContext===false){
  650. failCall("浏览器禁止不安全页面录音,可开启https解决");
  651. }else if(/Found/i.test(code)){//可能是非安全环境导致的没有设备
  652. failCall(msg+",无可用麦克风");
  653. }else{
  654. failCall(msg);
  655. };
  656. };
  657. //如果已打开并且有效就不要再打开了
  658. if(Recorder.IsOpen()){
  659. ok();
  660. return;
  661. };
  662. if(!Recorder.Support()){
  663. codeFail("","此浏览器不支持录音");
  664. return;
  665. };
  666. //请求权限,如果从未授权,一般浏览器会弹出权限请求弹框
  667. var f1=function(stream){
  668. //https://github.com/xiangyuecn/Recorder/issues/14 获取到的track.readyState!="live",刚刚回调时可能是正常的,但过一下可能就被关掉了,原因不明。延迟一下保证真异步。对正常浏览器不影响
  669. setTimeout(function(){
  670. stream._call={};
  671. var oldStream=Recorder.Stream;
  672. if(oldStream){
  673. Disconnect(); //直接断开已存在的,旧的Connect未完成会自动终止
  674. stream._call=oldStream._call;
  675. };
  676. Recorder.Stream=stream;
  677. if(lockFail())return;
  678. if(Recorder.IsOpen()){
  679. if(oldStream)This.CLog("发现同时多次调用open",1);
  680. Connect(streamStore,1);
  681. ok();
  682. }else{
  683. failCall("录音功能无效:无音频流");
  684. };
  685. },100);
  686. };
  687. var f2=function(e){
  688. var code=e.name||e.message||e.code+":"+e;
  689. This.CLog("请求录音权限错误",1,e);
  690. codeFail(code,"无法录音:"+code);
  691. };
  692. var trackSet={
  693. noiseSuppression:false //默认禁用降噪,原声录制,免得移动端表现怪异(包括系统播放声音变小)
  694. ,echoCancellation:false //回声消除
  695. };
  696. var trackSet2=This.set.audioTrackSet;
  697. for(var k in trackSet2)trackSet[k]=trackSet2[k];
  698. trackSet.sampleRate=Recorder.Ctx.sampleRate;//必须指明采样率,不然手机上MediaRecorder采样率16k
  699. try{
  700. var pro=Recorder.Scope[getUserMediaTxt]({audio:trackSet},f1,f2);
  701. }catch(e){//不能设置trackSet就算了
  702. This.CLog(getUserMediaTxt,3,e);
  703. pro=Recorder.Scope[getUserMediaTxt]({audio:true},f1,f2);
  704. };
  705. if(pro&&pro.then){
  706. pro.then(f1)[CatchTxt](f2); //fix 关键字,保证catch压缩时保持字符串形式
  707. };
  708. }
  709. //关闭释放录音资源
  710. ,close:function(call){
  711. call=call||NOOP;
  712. var This=this,streamStore=This._streamStore();
  713. This._stop();
  714. var Lock=streamStore.Sync;
  715. This._O=0;
  716. if(This._O_!=Lock.O){
  717. //唯一资源Stream的控制权已交给新对象,这里不能关闭。此处在每次都弹权限的浏览器内可能存在泄漏,新对象被拒绝权限可能不会调用close,忽略这种不处理
  718. This.CLog("close被忽略(因为同时open了多个rec,只有最后一个会真正close)",3);
  719. call();
  720. return;
  721. };
  722. Lock.C++;//获得控制权
  723. Disconnect(streamStore);
  724. This.CLog("close");
  725. call();
  726. }
  727. /*模拟一段录音数据,后面可以调用stop进行编码,需提供pcm数据[1,2,3...],pcm的采样率*/
  728. ,mock:function(pcmData,pcmSampleRate){
  729. var This=this;
  730. This._stop();//清理掉已有的资源
  731. This.isMock=1;
  732. This.mockEnvInfo=null;
  733. This.buffers=[pcmData];
  734. This.recSize=pcmData.length;
  735. This[srcSampleRateTxt]=pcmSampleRate;
  736. return This;
  737. }
  738. ,envCheck:function(envInfo){//平台环境下的可用性检查,任何时候都可以调用检查,返回errMsg:""正常,"失败原因"
  739. //envInfo={envName:"H5",canProcess:true}
  740. var errMsg,This=this,set=This.set;
  741. //检测CPU的数字字节序,TypedArray字节序是个迷,直接拒绝罕见的大端模式,因为找不到这种CPU进行测试
  742. var tag="CPU_BE";
  743. if(!errMsg && !Recorder[tag] && window.Int8Array && !new Int8Array(new Int32Array([1]).buffer)[0]){
  744. Traffic(tag); //如果开启了流量统计,这里将发送一个图片请求
  745. errMsg="不支持"+tag+"架构";
  746. };
  747. //编码器检查环境下配置是否可用
  748. if(!errMsg){
  749. var type=set.type;
  750. if(This[type+"_envCheck"]){//编码器已实现环境检查
  751. errMsg=This[type+"_envCheck"](envInfo,set);
  752. }else{//未实现检查的手动检查配置是否有效
  753. if(set.takeoffEncodeChunk){
  754. errMsg=type+"类型"+(This[type]?"":"(未加载编码器)")+"不支持设置takeoffEncodeChunk";
  755. };
  756. };
  757. };
  758. return errMsg||"";
  759. }
  760. ,envStart:function(mockEnvInfo,sampleRate){//平台环境相关的start调用
  761. var This=this,set=This.set;
  762. This.isMock=mockEnvInfo?1:0;//非H5环境需要启用mock,并提供envCheck需要的环境信息
  763. This.mockEnvInfo=mockEnvInfo;
  764. This.buffers=[];//数据缓冲
  765. This.recSize=0;//数据大小
  766. This.envInLast=0;//envIn接收到最后录音内容的时间
  767. This.envInFirst=0;//envIn接收到的首个录音内容的录制时间
  768. This.envInFix=0;//补偿的总时间
  769. This.envInFixTs=[];//补偿计数列表
  770. //engineCtx需要提前确定最终的采样率
  771. var setSr=set[sampleRateTxt];
  772. if(setSr>sampleRate){
  773. set[sampleRateTxt]=sampleRate;
  774. }else{ setSr=0 }
  775. This[srcSampleRateTxt]=sampleRate;
  776. This.CLog(srcSampleRateTxt+": "+sampleRate+" set."+sampleRateTxt+": "+set[sampleRateTxt]+(setSr?" 忽略"+setSr:""), setSr?3:0);
  777. This.engineCtx=0;
  778. //此类型有边录边转码(Worker)支持
  779. if(This[set.type+"_start"]){
  780. var engineCtx=This.engineCtx=This[set.type+"_start"](set);
  781. if(engineCtx){
  782. engineCtx.pcmDatas=[];
  783. engineCtx.pcmSize=0;
  784. };
  785. };
  786. }
  787. ,envResume:function(){//和平台环境无关的恢复录音
  788. //重新开始计数
  789. this.envInFixTs=[];
  790. }
  791. ,envIn:function(pcm,sum){//和平台环境无关的pcm[Int16]输入
  792. var This=this,set=This.set,engineCtx=This.engineCtx;
  793. var bufferSampleRate=This[srcSampleRateTxt];
  794. var size=pcm.length;
  795. var powerLevel=Recorder.PowerLevel(sum,size);
  796. var buffers=This.buffers;
  797. var bufferFirstIdx=buffers.length;//之前的buffer都是经过onProcess处理好的,不允许再修改
  798. buffers.push(pcm);
  799. //有engineCtx时会被覆盖,这里保存一份
  800. var buffersThis=buffers;
  801. var bufferFirstIdxThis=bufferFirstIdx;
  802. //卡顿丢失补偿:因为设备很卡的时候导致H5接收到的数据量不够造成播放时候变速,结果比实际的时长要短,此处保证了不会变短,但不能修复丢失的音频数据造成音质变差。当前算法采用输入时间侦测下一帧是否需要添加补偿帧,需要(6次输入||超过1秒)以上才会开始侦测,如果滑动窗口内丢失超过1/3就会进行补偿
  803. var now=Date.now();
  804. var pcmTime=Math.round(size/bufferSampleRate*1000);
  805. This.envInLast=now;
  806. if(This.buffers.length==1){//记下首个录音数据的录制时间
  807. This.envInFirst=now-pcmTime;
  808. };
  809. var envInFixTs=This.envInFixTs;
  810. envInFixTs.splice(0,0,{t:now,d:pcmTime});
  811. //保留3秒的计数滑动窗口,另外超过3秒的停顿不补偿
  812. var tsInStart=now,tsPcm=0;
  813. for(var i=0;i<envInFixTs.length;i++){
  814. var o=envInFixTs[i];
  815. if(now-o.t>3000){
  816. envInFixTs.length=i;
  817. break;
  818. };
  819. tsInStart=o.t;
  820. tsPcm+=o.d;
  821. };
  822. //达到需要的数据量,开始侦测是否需要补偿
  823. var tsInPrev=envInFixTs[1];
  824. var tsIn=now-tsInStart;
  825. var lost=tsIn-tsPcm;
  826. if( lost>tsIn/3 && (tsInPrev&&tsIn>1000 || envInFixTs.length>=6) ){
  827. //丢失过多,开始执行补偿
  828. var addTime=now-tsInPrev.t-pcmTime;//距离上次输入丢失这么多ms
  829. if(addTime>pcmTime/5){//丢失超过本帧的1/5
  830. var fixOpen=!set.disableEnvInFix;
  831. This.CLog("["+now+"]"+(fixOpen?"":"未")+"补偿"+addTime+"ms",3);
  832. This.envInFix+=addTime;
  833. //用静默进行补偿
  834. if(fixOpen){
  835. var addPcm=new Int16Array(addTime*bufferSampleRate/1000);
  836. size+=addPcm.length;
  837. buffers.push(addPcm);
  838. };
  839. };
  840. };
  841. var sizeOld=This.recSize,addSize=size;
  842. var bufferSize=sizeOld+addSize;
  843. This.recSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
  844. //此类型有边录边转码(Worker)支持,开启实时转码
  845. if(engineCtx){
  846. //转换成set的采样率
  847. var chunkInfo=Recorder.SampleData(buffers,bufferSampleRate,set[sampleRateTxt],engineCtx.chunkInfo);
  848. engineCtx.chunkInfo=chunkInfo;
  849. sizeOld=engineCtx.pcmSize;
  850. addSize=chunkInfo.data.length;
  851. bufferSize=sizeOld+addSize;
  852. engineCtx.pcmSize=bufferSize;//此值在onProcess后需要修正,可能新数据被修改
  853. buffers=engineCtx.pcmDatas;
  854. bufferFirstIdx=buffers.length;
  855. buffers.push(chunkInfo.data);
  856. bufferSampleRate=chunkInfo[sampleRateTxt];
  857. };
  858. var duration=Math.round(bufferSize/bufferSampleRate*1000);
  859. var bufferNextIdx=buffers.length;
  860. var bufferNextIdxThis=buffersThis.length;
  861. //允许异步处理buffer数据
  862. var asyncEnd=function(){
  863. //重新计算size,异步的早已减去添加的,同步的需去掉本次添加的然后重新计算
  864. var num=asyncBegin?0:-addSize;
  865. var hasClear=buffers[0]==null;
  866. for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
  867. var buffer=buffers[i];
  868. if(buffer==null){//已被主动释放内存,比如长时间实时传输录音时
  869. hasClear=1;
  870. }else{
  871. num+=buffer.length;
  872. //推入后台边录边转码
  873. if(engineCtx&&buffer.length){
  874. This[set.type+"_encode"](engineCtx,buffer);
  875. };
  876. };
  877. };
  878. //同步清理This.buffers,不管buffers到底清了多少个,buffersThis是使用不到的进行全清
  879. if(hasClear && engineCtx){
  880. var i=bufferFirstIdxThis;
  881. if(buffersThis[0]){
  882. i=0;
  883. };
  884. for(;i<bufferNextIdxThis;i++){
  885. buffersThis[i]=null;
  886. };
  887. };
  888. //统计修改后的size,如果异步发生clear要原样加回来,同步的无需操作
  889. if(hasClear){
  890. num=asyncBegin?addSize:0;
  891. buffers[0]=null;//彻底被清理
  892. };
  893. if(engineCtx){
  894. engineCtx.pcmSize+=num;
  895. }else{
  896. This.recSize+=num;
  897. };
  898. };
  899. //实时回调处理数据,允许修改或替换上次回调以来新增的数据 ,但是不允许修改已处理过的,不允许增删第一维数组 ,允许将第二维数组任意修改替换成空数组也可以
  900. var asyncBegin=0,procTxt="rec.set.onProcess";
  901. try{
  902. asyncBegin=set.onProcess(buffers,powerLevel,duration,bufferSampleRate,bufferFirstIdx,asyncEnd);
  903. }catch(e){
  904. //此错误显示不要用CLog,这样控制台内相同内容不会重复打印
  905. console.error(procTxt+"回调出错是不允许的,需保证不会抛异常",e);
  906. };
  907. var slowT=Date.now()-now;
  908. if(slowT>10 && This.envInFirst-now>1000){ //1秒后开始onProcess性能监测
  909. This.CLog(procTxt+"低性能,耗时"+slowT+"ms",3);
  910. };
  911. if(asyncBegin===true){
  912. //开启了异步模式,onProcess已接管buffers新数据,立即清空,避免出现未处理的数据
  913. var hasClear=0;
  914. for(var i=bufferFirstIdx;i<bufferNextIdx;i++){
  915. if(buffers[i]==null){//已被主动释放内存,比如长时间实时传输录音时 ,但又要开启异步模式,此种情况是非法的
  916. hasClear=1;
  917. }else{
  918. buffers[i]=new Int16Array(0);
  919. };
  920. };
  921. if(hasClear){
  922. This.CLog("未进入异步前不能清除buffers",3);
  923. }else{
  924. //还原size,异步结束后再统计仅修改后的size,如果发生clear要原样加回来
  925. if(engineCtx){
  926. engineCtx.pcmSize-=addSize;
  927. }else{
  928. This.recSize-=addSize;
  929. };
  930. };
  931. }else{
  932. asyncEnd();
  933. };
  934. }
  935. //开始录音,需先调用open;只要open成功时,调用此方法是安全的,如果未open强行调用导致的内部错误将不会有任何提示,stop时自然能得到错误
  936. ,start:function(){
  937. var This=this,ctx=Recorder.Ctx;
  938. var isOpen=1;
  939. if(This.set.sourceStream){//直接提供了流,仅判断是否调用了open
  940. if(!This.Stream){
  941. isOpen=0;
  942. }
  943. }else if(!Recorder.IsOpen()){//监测全局麦克风是否打开并且有效
  944. isOpen=0;
  945. };
  946. if(!isOpen){
  947. This.CLog("未open",1);
  948. return;
  949. };
  950. This.CLog("开始录音");
  951. This._stop();
  952. This.state=3;//0未录音 1录音中 2暂停 3等待ctx激活
  953. This.envStart(null, ctx[sampleRateTxt]);
  954. //检查open过程中stop是否已经调用过
  955. if(This._SO&&This._SO+1!=This._S){//上面调用过一次 _stop
  956. //open未完成就调用了stop,此种情况终止start。也应尽量避免出现此情况
  957. This.CLog("start被中断",3);
  958. return;
  959. };
  960. This._SO=0;
  961. var end=function(){
  962. if(This.state==3){
  963. This.state=1;
  964. This.resume();
  965. }
  966. };
  967. if(ctx.state=="suspended"){
  968. var tag="AudioContext resume: ";
  969. This.CLog(tag+"wait...");
  970. ctx.resume().then(function(){
  971. This.CLog(tag+ctx.state);
  972. end();
  973. })[CatchTxt](function(e){ //比较少见,可能对录音没有影响
  974. This.CLog(tag+ctx.state+" 可能无法录音:"+e.message,1,e);
  975. end();
  976. });
  977. }else{
  978. end();
  979. };
  980. }
  981. /*暂停录音*/
  982. ,pause:function(){
  983. var This=this;
  984. if(This.state){
  985. This.state=2;
  986. This.CLog("pause");
  987. delete This._streamStore().Stream._call[This.id];
  988. };
  989. }
  990. /*恢复录音*/
  991. ,resume:function(){
  992. var This=this;
  993. if(This.state){
  994. This.state=1;
  995. This.CLog("resume");
  996. This.envResume();
  997. var stream=This._streamStore().Stream;
  998. stream._call[This.id]=function(pcm,sum){
  999. if(This.state==1){
  1000. This.envIn(pcm,sum);
  1001. };
  1002. };
  1003. ConnAlive(stream);//AudioWorklet只会在ctx激活后运行
  1004. };
  1005. }
  1006. ,_stop:function(keepEngine){
  1007. var This=this,set=This.set;
  1008. if(!This.isMock){
  1009. This._S++;
  1010. };
  1011. if(This.state){
  1012. This.pause();
  1013. This.state=0;
  1014. };
  1015. if(!keepEngine && This[set.type+"_stop"]){
  1016. This[set.type+"_stop"](This.engineCtx);
  1017. This.engineCtx=0;
  1018. };
  1019. }
  1020. /*
  1021. 结束录音并返回录音数据blob对象
  1022. True(blob,duration) blob:录音数据audio/mp3|wav格式
  1023. duration:录音时长,单位毫秒
  1024. False(msg)
  1025. autoClose:false 可选,是否自动调用close,默认为false
  1026. */
  1027. ,stop:function(True,False,autoClose){
  1028. var This=this,set=This.set,t1;
  1029. var envInMS=This.envInLast-This.envInFirst, envInLen=envInMS&&This.buffers.length; //可能未start
  1030. This.CLog("stop 和start时差"+(envInMS?envInMS+"ms 补偿"+This.envInFix+"ms"+" envIn:"+envInLen+" fps:"+(envInLen/envInMS*1000).toFixed(1):"-"));
  1031. var end=function(){
  1032. This._stop();//彻底关掉engineCtx
  1033. if(autoClose){
  1034. This.close();
  1035. };
  1036. };
  1037. var err=function(msg){
  1038. This.CLog("结束录音失败:"+msg,1);
  1039. False&&False(msg);
  1040. end();
  1041. };
  1042. var ok=function(blob,duration){
  1043. This.CLog("结束录音 编码花"+(Date.now()-t1)+"ms 音频时长"+duration+"ms 文件大小"+blob.size+"b");
  1044. if(set.takeoffEncodeChunk){//接管了输出,此时blob长度为0
  1045. This.CLog("启用takeoffEncodeChunk后stop返回的blob长度为0不提供音频数据",3);
  1046. }else if(blob.size<Math.max(100,duration/2)){//1秒小于0.5k?
  1047. err("生成的"+set.type+"无效");
  1048. return;
  1049. };
  1050. True&&True(blob,duration);
  1051. end();
  1052. };
  1053. if(!This.isMock){
  1054. var isCtxWait=This.state==3;
  1055. if(!This.state || isCtxWait){
  1056. err("未开始录音"+(isCtxWait?",开始录音前无用户交互导致AudioContext未运行":""));
  1057. return;
  1058. };
  1059. This._stop(true);
  1060. };
  1061. var size=This.recSize;
  1062. if(!size){
  1063. err("未采集到录音");
  1064. return;
  1065. };
  1066. if(!This.buffers[0]){
  1067. err("音频buffers被释放");
  1068. return;
  1069. };
  1070. if(!This[set.type]){
  1071. err("未加载"+set.type+"编码器");
  1072. return;
  1073. };
  1074. //环境配置检查,此处仅针对mock调用,因为open已经检查过了
  1075. if(This.isMock){
  1076. var checkMsg=This.envCheck(This.mockEnvInfo||{envName:"mock",canProcess:false});//没有提供环境信息的mock时没有onProcess回调
  1077. if(checkMsg){
  1078. err("录音错误:"+checkMsg);
  1079. return;
  1080. };
  1081. };
  1082. //此类型有边录边转码(Worker)支持
  1083. var engineCtx=This.engineCtx;
  1084. if(This[set.type+"_complete"]&&engineCtx){
  1085. var duration=Math.round(engineCtx.pcmSize/set[sampleRateTxt]*1000);//采用后的数据长度和buffers的长度可能微小的不一致,是采样率连续转换的精度问题
  1086. t1=Date.now();
  1087. This[set.type+"_complete"](engineCtx,function(blob){
  1088. ok(blob,duration);
  1089. },err);
  1090. return;
  1091. };
  1092. //标准UI线程转码,调整采样率
  1093. t1=Date.now();
  1094. var chunk=Recorder.SampleData(This.buffers,This[srcSampleRateTxt],set[sampleRateTxt]);
  1095. set[sampleRateTxt]=chunk[sampleRateTxt];
  1096. var res=chunk.data;
  1097. var duration=Math.round(res.length/set[sampleRateTxt]*1000);
  1098. This.CLog("采样"+size+"->"+res.length+" 花:"+(Date.now()-t1)+"ms");
  1099. setTimeout(function(){
  1100. t1=Date.now();
  1101. This[set.type](res,function(blob){
  1102. ok(blob,duration);
  1103. },function(msg){
  1104. err(msg);
  1105. });
  1106. });
  1107. }
  1108. };
  1109. if(window[RecTxt]){
  1110. CLog("重复引入"+RecTxt,3);
  1111. window[RecTxt].Destroy();
  1112. };
  1113. window[RecTxt]=Recorder;
  1114. //=======从WebM字节流中提取pcm数据,提取成功返回Float32Array,失败返回null||-1=====
  1115. var WebM_Extract=function(inBytes, scope){
  1116. if(!scope.pos){
  1117. scope.pos=[0]; scope.tracks={}; scope.bytes=[];
  1118. };
  1119. var tracks=scope.tracks, position=[scope.pos[0]];
  1120. var endPos=function(){ scope.pos[0]=position[0] };
  1121. var sBL=scope.bytes.length;
  1122. var bytes=new Uint8Array(sBL+inBytes.length);
  1123. bytes.set(scope.bytes); bytes.set(inBytes,sBL);
  1124. scope.bytes=bytes;
  1125. //先读取文件头和Track信息
  1126. if(!scope._ht){
  1127. readMatroskaVInt(bytes, position);//EBML Header
  1128. readMatroskaBlock(bytes, position);//跳过EBML Header内容
  1129. if(!BytesEq(readMatroskaVInt(bytes, position), [0x18,0x53,0x80,0x67])){
  1130. return;//未识别到Segment
  1131. }
  1132. readMatroskaVInt(bytes, position);//跳过Segment长度值
  1133. while(position[0]<bytes.length){
  1134. var eid0=readMatroskaVInt(bytes, position);
  1135. var bytes0=readMatroskaBlock(bytes, position);
  1136. var pos0=[0],audioIdx=0;
  1137. if(!bytes0)return;//数据不全,等待缓冲
  1138. //Track完整数据,循环读取TrackEntry
  1139. if(BytesEq(eid0, [0x16,0x54,0xAE,0x6B])){
  1140. while(pos0[0]<bytes0.length){
  1141. var eid1=readMatroskaVInt(bytes0, pos0);
  1142. var bytes1=readMatroskaBlock(bytes0, pos0);
  1143. var pos1=[0],track={channels:0,sampleRate:0};
  1144. if(BytesEq(eid1, [0xAE])){//TrackEntry
  1145. while(pos1[0]<bytes1.length){
  1146. var eid2=readMatroskaVInt(bytes1, pos1);
  1147. var bytes2=readMatroskaBlock(bytes1, pos1);
  1148. var pos2=[0];
  1149. if(BytesEq(eid2, [0xD7])){//Track Number
  1150. var val=BytesInt(bytes2);
  1151. track.number=val;
  1152. tracks[val]=track;
  1153. }else if(BytesEq(eid2, [0x83])){//Track Type
  1154. var val=BytesInt(bytes2);
  1155. if(val==1) track.type="video";
  1156. else if(val==2) {
  1157. track.type="audio";
  1158. if(!audioIdx) scope.track0=track;
  1159. track.idx=audioIdx++;
  1160. }else track.type="Type-"+val;
  1161. }else if(BytesEq(eid2, [0x86])){//Track Codec
  1162. var str="";
  1163. for(var i=0;i<bytes2.length;i++){
  1164. str+=String.fromCharCode(bytes2[i]);
  1165. }
  1166. track.codec=str;
  1167. }else if(BytesEq(eid2, [0xE1])){
  1168. while(pos2[0]<bytes2.length){//循环读取 Audio 属性
  1169. var eid3=readMatroskaVInt(bytes2, pos2);
  1170. var bytes3=readMatroskaBlock(bytes2, pos2);
  1171. //采样率、位数、声道数
  1172. if(BytesEq(eid3, [0xB5])){
  1173. var val=0,arr=new Uint8Array(bytes3.reverse()).buffer;
  1174. if(bytes3.length==4) val=new Float32Array(arr)[0];
  1175. else if(bytes3.length==8) val=new Float64Array(arr)[0];
  1176. else CLog("WebM Track !Float",1,bytes3);
  1177. track[sampleRateTxt]=Math.round(val);
  1178. }else if(BytesEq(eid3, [0x62,0x64])) track.bitDepth=BytesInt(bytes3);
  1179. else if(BytesEq(eid3, [0x9F])) track.channels=BytesInt(bytes3);
  1180. }
  1181. }
  1182. }
  1183. }
  1184. };
  1185. scope._ht=1;
  1186. CLog("WebM Tracks",tracks);
  1187. endPos();
  1188. break;
  1189. }
  1190. }
  1191. }
  1192. //校验音频参数信息,如果不符合代码要求,统统拒绝处理
  1193. var track0=scope.track0;
  1194. if(!track0)return;
  1195. if(track0.bitDepth==16 && /FLOAT/i.test(track0.codec)){
  1196. track0.bitDepth=32; //chrome v66 实际为浮点数
  1197. CLog("WebM 16改32位",3);
  1198. }
  1199. if(track0[sampleRateTxt]!=scope[sampleRateTxt] || track0.bitDepth!=32 || track0.channels<1 || !/(\b|_)PCM\b/i.test(track0.codec)){
  1200. scope.bytes=[];//格式非预期 无法处理,清空缓冲数据
  1201. if(!scope.bad)CLog("WebM Track非预期",3,scope);
  1202. scope.bad=1;
  1203. return -1;
  1204. }
  1205. //循环读取Cluster内的SimpleBlock
  1206. var datas=[],dataLen=0;
  1207. while(position[0]<bytes.length){
  1208. var eid1=readMatroskaVInt(bytes, position);
  1209. var bytes1=readMatroskaBlock(bytes, position);
  1210. if(!bytes1)break;//数据不全,等待缓冲
  1211. if(BytesEq(eid1, [0xA3])){//SimpleBlock完整数据
  1212. var trackNo=bytes1[0]&0xf;
  1213. var track=tracks[trackNo];
  1214. if(!track){//不可能没有,数据出错?
  1215. CLog("WebM !Track"+trackNo,1,tracks);
  1216. }else if(track.idx===0){
  1217. var u8arr=new Uint8Array(bytes1.length-4);
  1218. for(var i=4;i<bytes1.length;i++){
  1219. u8arr[i-4]=bytes1[i];
  1220. }
  1221. datas.push(u8arr); dataLen+=u8arr.length;
  1222. }
  1223. }
  1224. endPos();
  1225. }
  1226. if(dataLen){
  1227. var more=new Uint8Array(bytes.length-scope.pos[0]);
  1228. more.set(bytes.subarray(scope.pos[0]));
  1229. scope.bytes=more; //清理已读取了的缓冲数据
  1230. scope.pos[0]=0;
  1231. var u8arr=new Uint8Array(dataLen); //已获取的音频数据
  1232. for(var i=0,i2=0;i<datas.length;i++){
  1233. u8arr.set(datas[i],i2);
  1234. i2+=datas[i].length;
  1235. }
  1236. var arr=new Float32Array(u8arr.buffer);
  1237. if(track0.channels>1){//多声道,提取一个声道
  1238. var arr2=[];
  1239. for(var i=0;i<arr.length;){
  1240. arr2.push(arr[i]);
  1241. i+=track0.channels;
  1242. }
  1243. arr=new Float32Array(arr2);
  1244. };
  1245. return arr;
  1246. }
  1247. };
  1248. //两个字节数组内容是否相同
  1249. var BytesEq=function(bytes1,bytes2){
  1250. if(!bytes1 || bytes1.length!=bytes2.length) return false;
  1251. if(bytes1.length==1) return bytes1[0]==bytes2[0];
  1252. for(var i=0;i<bytes1.length;i++){
  1253. if(bytes1[i]!=bytes2[i]) return false;
  1254. }
  1255. return true;
  1256. };
  1257. //字节数组BE转成int数字
  1258. var BytesInt=function(bytes){
  1259. var s="";//0-8字节,js位运算只支持4字节
  1260. for(var i=0;i<bytes.length;i++){var n=bytes[i];s+=(n<16?"0":"")+n.toString(16)};
  1261. return parseInt(s,16)||0;
  1262. };
  1263. //读取一个可变长数值字节数组
  1264. var readMatroskaVInt=function(arr,pos,trim){
  1265. var i=pos[0];
  1266. if(i>=arr.length)return;
  1267. var b0=arr[i],b2=("0000000"+b0.toString(2)).substr(-8);
  1268. var m=/^(0*1)(\d*)$/.exec(b2);
  1269. if(!m)return;
  1270. var len=m[1].length, val=[];
  1271. if(i+len>arr.length)return;
  1272. for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
  1273. if(trim) val[0]=parseInt(m[2]||'0',2);
  1274. pos[0]=i;
  1275. return val;
  1276. };
  1277. //读取一个自带长度的内容字节数组
  1278. var readMatroskaBlock=function(arr,pos){
  1279. var lenVal=readMatroskaVInt(arr,pos,1);
  1280. if(!lenVal)return;
  1281. var len=BytesInt(lenVal);
  1282. var i=pos[0], val=[];
  1283. if(len<0x7FFFFFFF){ //超大值代表没有长度
  1284. if(i+len>arr.length)return;
  1285. for(var i2=0;i2<len;i2++){ val[i2]=arr[i]; i++; }
  1286. }
  1287. pos[0]=i;
  1288. return val;
  1289. };
  1290. //=====End WebM读取=====
  1291. //流量统计用1像素图片地址,设置为空将不参与统计
  1292. Recorder.TrafficImgUrl="//ia.51.la/go1?id=20469973&pvFlag=1";
  1293. var Traffic=Recorder.Traffic=function(report){
  1294. report=report?"/"+RecTxt+"/Report/"+report:"";
  1295. var imgUrl=Recorder.TrafficImgUrl;
  1296. if(imgUrl){
  1297. var data=Recorder.Traffic;
  1298. var m=/^(https?:..[^\/#]*\/?)[^#]*/i.exec(location.href)||[];
  1299. var host=(m[1]||"http://file/");
  1300. var idf=(m[0]||host)+report;
  1301. if(imgUrl.indexOf("//")==0){
  1302. //给url加上http前缀,如果是file协议下,不加前缀没法用
  1303. if(/^https:/i.test(idf)){
  1304. imgUrl="https:"+imgUrl;
  1305. }else{
  1306. imgUrl="http:"+imgUrl;
  1307. };
  1308. };
  1309. if(report){
  1310. imgUrl=imgUrl+"&cu="+encodeURIComponent(host+report);
  1311. };
  1312. if(!data[idf]){
  1313. data[idf]=1;
  1314. var img=new Image();
  1315. img.src=imgUrl;
  1316. CLog("Traffic Analysis Image: "+(report||RecTxt+".TrafficImgUrl="+Recorder.TrafficImgUrl));
  1317. };
  1318. };
  1319. };
  1320. }));