mp3.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /*
  2. mp3编码器,需带上src/engine/mp3-engine.js引擎使用
  3. https://github.com/xiangyuecn/Recorder
  4. 当然最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式
  5. 浏览器支持情况
  6. https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
  7. */
  8. (function(factory){
  9. var browser=typeof window=="object" && !!window.document;
  10. var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
  11. var rec=win.Recorder,ni=rec.i18n;
  12. factory(rec,ni,ni.$T,browser);
  13. }(function(Recorder,i18n,$T,isBrowser){
  14. "use strict";
  15. var SampleS="48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000";
  16. var BitS="8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320";
  17. Recorder.prototype.enc_mp3={
  18. stable:true,takeEC:"full"
  19. ,getTestMsg:function(){
  20. return $T("Zm7L::采样率范围:{1};比特率范围:{2}(不同比特率支持的采样率范围不同,小于32kbps时采样率需小于32000)",0,SampleS,BitS);
  21. }
  22. };
  23. var NormalizeSet=function(set){
  24. var bS=set.bitRate, sS=set.sampleRate,s=sS;
  25. if((" "+BitS+",").indexOf(" "+bS+",")==-1){
  26. Recorder.CLog($T("eGB9::{1}不在mp3支持的取值范围:{2}",0,"bitRate="+bS,BitS),3);
  27. }
  28. if((" "+SampleS+",").indexOf(" "+sS+",")==-1){//engine SmpFrqIndex函数会检测
  29. var arr=SampleS.split(", "),vs=[];
  30. for(var i=0;i<arr.length;i++) vs.push({v:+arr[i],s:Math.abs(arr[i]-sS)});
  31. vs.sort(function(a,b){return a.s-b.s}); s=vs[0].v;
  32. set.sampleRate=s;
  33. Recorder.CLog($T("zLTa::sampleRate已更新为{1},因为{2}不在mp3支持的取值范围:{3}",0,s,sS,SampleS),3);
  34. }
  35. };
  36. var ImportEngineErr=function(){
  37. return $T.G("NeedImport-2",["mp3.js","src/engine/mp3-engine.js"]);
  38. };
  39. //是否支持web worker
  40. var HasWebWorker=isBrowser && typeof Worker=="function";
  41. //*******标准UI线程转码支持函数************
  42. Recorder.prototype.mp3=function(res,True,False){
  43. var This=this,set=This.set,size=res.length;
  44. if(!Recorder.lamejs){
  45. False(ImportEngineErr()); return;
  46. };
  47. //优先采用worker编码,非worker时用老方法提供兼容
  48. if(HasWebWorker){
  49. var ctx=This.mp3_start(set);
  50. if(ctx){
  51. if(ctx.isW){
  52. This.mp3_encode(ctx,res);
  53. This.mp3_complete(ctx,True,False,1);
  54. return;
  55. }
  56. This.mp3_stop(ctx);
  57. };
  58. };
  59. NormalizeSet(set);
  60. //https://github.com/wangpengfei15975/recorder.js
  61. //https://github.com/zhuker/lamejs bug:采样率必须和源一致,不然8k时没有声音,有问题fix:https://github.com/zhuker/lamejs/pull/11
  62. var mp3=new Recorder.lamejs.Mp3Encoder(1,set.sampleRate,set.bitRate);
  63. var blockSize=57600;
  64. var memory=new Int8Array(500000), mOffset=0;
  65. var idx=0,isFlush=0;
  66. var run=function(){
  67. try{
  68. if(idx<size){
  69. var buf=mp3.encodeBuffer(res.subarray(idx,idx+blockSize));
  70. }else{
  71. isFlush=1;
  72. var buf=mp3.flush();
  73. };
  74. }catch(e){ //精简代码调用了abort
  75. console.error(e);
  76. if(!isFlush) try{ mp3.flush() }catch(r){ console.error(r) }
  77. False("MP3 Encoder: "+e.message);
  78. return;
  79. };
  80. var bufLen=buf.length;
  81. if(bufLen>0){
  82. if(mOffset+bufLen>memory.length){
  83. var tmp=new Int8Array(memory.length+Math.max(500000,bufLen));
  84. tmp.set(memory.subarray(0, mOffset));
  85. memory=tmp;
  86. }
  87. memory.set(buf,mOffset);
  88. mOffset+=bufLen;
  89. };
  90. if(idx<size){
  91. idx+=blockSize;
  92. setTimeout(run);//尽量避免卡ui
  93. }else{
  94. var data=[memory.buffer.slice(0,mOffset)];
  95. //去掉开头的标记信息帧
  96. var meta=mp3TrimFix.fn(data,mOffset,size,set.sampleRate);
  97. mp3TrimFixSetMeta(meta,set);
  98. True(data[0]||new ArrayBuffer(0),"audio/mp3");
  99. };
  100. };
  101. run();
  102. }
  103. //********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码*********
  104. //全局共享一个Worker,后台串行执行。如果每次都开一个新的,编码速度可能会慢很多,可能是浏览器运行缓存的因素,并且可能瞬间产生多个并行操作占用大量cpu
  105. var mp3Worker;
  106. Recorder.BindDestroy("mp3Worker",function(){
  107. if(mp3Worker){
  108. Recorder.CLog("mp3Worker Destroy");
  109. mp3Worker.terminate();
  110. mp3Worker=null;
  111. };
  112. });
  113. Recorder.prototype.mp3_envCheck=function(envInfo,set){//检查环境下配置是否可用
  114. var errMsg="";
  115. //需要实时编码返回数据,此时需要检查是否可实时编码
  116. if(set.takeoffEncodeChunk){
  117. if(!newContext()){//浏览器不能创建实时编码环境
  118. errMsg=$T("yhUs::当前浏览器版本太低,无法实时处理");
  119. };
  120. };
  121. if(!errMsg && !Recorder.lamejs){
  122. errMsg=ImportEngineErr();
  123. };
  124. return errMsg;
  125. };
  126. Recorder.prototype.mp3_start=function(set){//如果返回null代表不支持
  127. return newContext(set);
  128. };
  129. var openList={id:0};
  130. var newContext=function(setOrNull,_badW){
  131. //独立运行的函数,scope.wkScope worker.onmessage 字符串会被替换
  132. var run=function(e){
  133. var ed=e.data;
  134. var wk_ctxs=scope.wkScope.wk_ctxs;
  135. var wk_lame=scope.wkScope.wk_lame;
  136. var wk_mp3TrimFix=scope.wkScope.wk_mp3TrimFix;
  137. var cur=wk_ctxs[ed.id];
  138. if(ed.action=="init"){
  139. wk_ctxs[ed.id]={
  140. sampleRate:ed.sampleRate
  141. ,bitRate:ed.bitRate
  142. ,takeoff:ed.takeoff
  143. ,pcmSize:0
  144. ,memory:new Int8Array(500000), mOffset:0
  145. ,encObj:new wk_lame.Mp3Encoder(1,ed.sampleRate,ed.bitRate)
  146. };
  147. }else if(!cur){
  148. return;
  149. };
  150. var addBytes=function(buf){
  151. var bufLen=buf.length;
  152. if(cur.mOffset+bufLen>cur.memory.length){
  153. var tmp=new Int8Array(cur.memory.length+Math.max(500000,bufLen));
  154. tmp.set(cur.memory.subarray(0, cur.mOffset));
  155. cur.memory=tmp;
  156. }
  157. cur.memory.set(buf,cur.mOffset);
  158. cur.mOffset+=bufLen;
  159. };
  160. switch(ed.action){
  161. case "stop":
  162. if(!cur.isCp) try{ cur.encObj.flush() }catch(e){ console.error(e) }
  163. cur.encObj=null;
  164. delete wk_ctxs[ed.id];
  165. break;
  166. case "encode":
  167. if(cur.isCp)break;
  168. cur.pcmSize+=ed.pcm.length;
  169. try{
  170. var buf=cur.encObj.encodeBuffer(ed.pcm);
  171. }catch(e){ //精简代码调用了abort
  172. cur.err=e;
  173. console.error(e);
  174. };
  175. if(buf && buf.length>0){
  176. if(cur.takeoff){
  177. worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
  178. }else{
  179. addBytes(buf);
  180. };
  181. };
  182. break;
  183. case "complete":
  184. cur.isCp=1;
  185. try{
  186. var buf=cur.encObj.flush();
  187. }catch(e){ //精简代码调用了abort
  188. cur.err=e;
  189. console.error(e);
  190. };
  191. if(buf && buf.length>0){
  192. if(cur.takeoff){
  193. worker.onmessage({action:"takeoff",id:ed.id,chunk:buf});
  194. }else{
  195. addBytes(buf);
  196. };
  197. };
  198. if(cur.err){
  199. worker.onmessage({action:ed.action,id:ed.id
  200. ,err:"MP3 Encoder: "+cur.err.message});
  201. break;
  202. };
  203. var data=[cur.memory.buffer.slice(0,cur.mOffset)];
  204. //去掉开头的标记信息帧
  205. var meta=wk_mp3TrimFix.fn(data,cur.mOffset,cur.pcmSize,cur.sampleRate);
  206. worker.onmessage({
  207. action:ed.action
  208. ,id:ed.id
  209. ,blob:data[0]||new ArrayBuffer(0)
  210. ,meta:meta
  211. });
  212. break;
  213. };
  214. };
  215. var initOnMsg=function(isW){
  216. worker.onmessage=function(e){
  217. var data=e; if(isW)data=e.data;
  218. var ctx=openList[data.id];
  219. if(ctx){
  220. if(data.action=="takeoff"){
  221. //取走实时生成的mp3数据
  222. ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer));
  223. }else{
  224. //complete
  225. ctx.call&&ctx.call(data);
  226. ctx.call=null;
  227. };
  228. };
  229. };
  230. };
  231. var initCtx=function(){
  232. var ctx={worker:worker,set:setOrNull};
  233. if(setOrNull){
  234. ctx.id=++openList.id;
  235. openList[ctx.id]=ctx;
  236. NormalizeSet(setOrNull);
  237. worker.postMessage({
  238. action:"init"
  239. ,id:ctx.id
  240. ,sampleRate:setOrNull.sampleRate
  241. ,bitRate:setOrNull.bitRate
  242. ,takeoff:!!setOrNull.takeoffEncodeChunk
  243. ,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
  244. });
  245. }else{
  246. worker.postMessage({
  247. x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
  248. });
  249. };
  250. return ctx;
  251. };
  252. var scope,worker=mp3Worker;
  253. //非浏览器,不支持worker,或者开启失败,使用UI线程处理
  254. if(_badW || !HasWebWorker){
  255. Recorder.CLog($T("k9PT::当前环境不支持Web Worker,mp3实时编码器运行在主线程中"),3);
  256. worker={ postMessage:function(ed){ run({data:ed}); } };
  257. scope={wkScope:{
  258. wk_ctxs:{}, wk_lame:Recorder.lamejs, wk_mp3TrimFix:mp3TrimFix
  259. }};
  260. initOnMsg();
  261. return initCtx();
  262. };
  263. try{
  264. if(!worker){
  265. //创建一个新Worker
  266. var onmsg=(run+"").replace(/[\w\$]+\.onmessage/g,"self.postMessage");
  267. onmsg=onmsg.replace(/[\w\$]+\.wkScope/g,"wkScope");
  268. var jsCode=");wk_lame();self.onmessage="+onmsg;
  269. jsCode+=";var wkScope={ wk_ctxs:{},wk_lame:wk_lame";
  270. jsCode+=",wk_mp3TrimFix:{rm:"+mp3TrimFix.rm+",fn:"+mp3TrimFix.fn+"} }";
  271. var lamejsCode=Recorder.lamejs.toString();
  272. var url=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_lame=(",lamejsCode,jsCode], {type:"text/javascript"}));
  273. worker=new Worker(url);
  274. setTimeout(function(){
  275. (window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
  276. },10000);//chrome 83 file协议下如果直接释放,将会使WebWorker无法启动
  277. initOnMsg(1);
  278. };
  279. var ctx=initCtx(); ctx.isW=1;
  280. mp3Worker=worker;
  281. return ctx;
  282. }catch(e){//出错了就不要提供了
  283. worker&&worker.terminate();
  284. console.error(e);
  285. return newContext(setOrNull, 1);//切换到UI线程处理
  286. };
  287. };
  288. Recorder.prototype.mp3_stop=function(startCtx){
  289. if(startCtx&&startCtx.worker){
  290. startCtx.worker.postMessage({
  291. action:"stop"
  292. ,id:startCtx.id
  293. });
  294. startCtx.worker=null;
  295. delete openList[startCtx.id];
  296. //疑似泄露检测 排除id
  297. var opens=-1;
  298. for(var k in openList){
  299. opens++;
  300. };
  301. if(opens){
  302. Recorder.CLog($T("fT6M::mp3 worker剩{1}个未stop",0,opens),3);
  303. };
  304. };
  305. };
  306. Recorder.prototype.mp3_encode=function(startCtx,pcm){
  307. if(startCtx&&startCtx.worker){
  308. startCtx.worker.postMessage({
  309. action:"encode"
  310. ,id:startCtx.id
  311. ,pcm:pcm
  312. });
  313. };
  314. };
  315. Recorder.prototype.mp3_complete=function(startCtx,True,False,autoStop){
  316. var This=this;
  317. if(startCtx&&startCtx.worker){
  318. startCtx.call=function(data){
  319. if(autoStop){
  320. This.mp3_stop(startCtx);
  321. };
  322. if(data.err){
  323. False(data.err);
  324. }else{
  325. mp3TrimFixSetMeta(data.meta,startCtx.set);
  326. True(data.blob,"audio/mp3");
  327. };
  328. };
  329. startCtx.worker.postMessage({
  330. action:"complete"
  331. ,id:startCtx.id
  332. });
  333. }else{
  334. False($T("mPxH::mp3编码器未start"));
  335. };
  336. };
  337. //*******辅助函数************
  338. /*读取lamejs编码出来的mp3信息,只能读特定格式,如果读取失败返回null
  339. mp3Buffers=[ArrayBuffer,...]
  340. length=mp3Buffers的数据二进制总长度
  341. */
  342. Recorder.mp3ReadMeta=function(mp3Buffers,length){
  343. //kill babel-polyfill ES6 Number.parseInt 不然放到Worker里面找不到方法,也不能用typeof(x)==object 会被替换成 _typeof
  344. var parseInt_ES3=typeof(window)!="undefined"&&window.parseInt||typeof(self)!="undefined"&&self.parseInt||parseInt;
  345. var u8arr0=new Uint8Array(mp3Buffers[0]||[]);
  346. if(u8arr0.length<4){
  347. return null;
  348. };
  349. var byteAt=function(idx,u8){
  350. return ("0000000"+((u8||u8arr0)[idx]||0).toString(2)).substr(-8);
  351. };
  352. var b2=byteAt(0)+byteAt(1);
  353. var b4=byteAt(2)+byteAt(3);
  354. if(!/^1{11}/.test(b2)){//未发现帧同步
  355. return null;
  356. };
  357. var version=({"00":2.5,"10":2,"11":1})[b2.substr(11,2)];
  358. var layer=({"01":3})[b2.substr(13,2)];//仅支持Layer3
  359. var sampleRate=({ //lamejs -> Tables.samplerate_table
  360. "1":[44100, 48000, 32000]
  361. ,"2":[22050, 24000, 16000]
  362. ,"2.5":[11025, 12000, 8000]
  363. })[version];
  364. sampleRate&&(sampleRate=sampleRate[parseInt_ES3(b4.substr(4,2),2)]);
  365. var bitRate=[ //lamejs -> Tables.bitrate_table
  366. [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] //MPEG 2 2.5
  367. ,[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]//MPEG 1
  368. ][version==1?1:0][parseInt_ES3(b4.substr(0,4),2)];
  369. if(!version || !layer || !bitRate || !sampleRate){
  370. return null;
  371. };
  372. var duration=Math.round(length*8/bitRate);
  373. var frame=layer==1?384:layer==2?1152:version==1?1152:576;
  374. var frameDurationFloat=frame/sampleRate*1000;
  375. var frameSize=Math.floor((frame*bitRate)/8/sampleRate*1000);
  376. //检测是否存在Layer3帧填充1字节。这里只获取第二帧的填充信息,首帧永远没有填充。其他帧可能隔一帧出现一个填充,或者隔很多帧出现一个填充;目测是取决于frameSize未舍入时的小数部分,因为有些采样率的frameSize会出现小数(11025、22050、44100 典型的除不尽),然后字节数无法表示这种小数,就通过一定步长来填充弥补小数部分丢失
  377. var hasPadding=0,seek=0;
  378. for(var i=0;i<mp3Buffers.length;i++){
  379. //寻找第二帧
  380. var buf=mp3Buffers[i];
  381. seek+=buf.byteLength;
  382. if(seek>=frameSize+3){
  383. var buf8=new Uint8Array(buf);
  384. var idx=buf.byteLength-(seek-(frameSize+3)+1);
  385. var ib4=byteAt(idx,buf8);
  386. hasPadding=ib4.charAt(6)=="1";
  387. break;
  388. };
  389. };
  390. if(hasPadding){
  391. frameSize++;
  392. };
  393. return {
  394. version:version //1 2 2.5 -> MPEG1 MPEG2 MPEG2.5
  395. ,layer:layer//3 -> Layer3
  396. ,sampleRate:sampleRate //采样率 hz
  397. ,bitRate:bitRate //比特率 kbps
  398. ,duration:duration //音频时长 ms
  399. ,size:length //总长度 byte
  400. ,hasPadding:hasPadding //是否存在1字节填充,首帧永远没有,这个值其实代表的第二帧是否有填充,并不代表其他帧的
  401. ,frameSize:frameSize //每帧最大长度,含可能存在的1字节padding byte
  402. ,frameDurationFloat:frameDurationFloat //每帧时长,含小数 ms
  403. };
  404. };
  405. //去掉lamejs开头的标记信息帧,免得mp3解码出来的时长比pcm的长太多
  406. var mp3TrimFix={//minfiy keep name
  407. rm:Recorder.mp3ReadMeta
  408. ,fn:function(mp3Buffers,length,pcmLength,pcmSampleRate){
  409. var meta=this.rm(mp3Buffers,length);
  410. if(!meta){
  411. return {size:length, err:"mp3 unknown format"};
  412. };
  413. var pcmDuration=Math.round(pcmLength/pcmSampleRate*1000);
  414. //开头多出这么多帧,移除掉;正常情况下最多为2帧
  415. var num=Math.floor((meta.duration-pcmDuration)/meta.frameDurationFloat);
  416. if(num>0){
  417. var size=num*meta.frameSize-(meta.hasPadding?1:0);//首帧没有填充,第二帧可能有填充,这里假设最多为2帧(测试并未出现3帧以上情况),其他帧不管,就算出现了并且导致了错误后面自动容错
  418. length-=size;
  419. var arr0=0,arrs=[];
  420. for(var i=0;i<mp3Buffers.length;i++){
  421. var arr=mp3Buffers[i];
  422. if(size<=0){
  423. break;
  424. };
  425. if(size>=arr.byteLength){
  426. size-=arr.byteLength;
  427. arrs.push(arr);
  428. mp3Buffers.splice(i,1);
  429. i--;
  430. }else{
  431. mp3Buffers[i]=arr.slice(size);
  432. arr0=arr;
  433. size=0;
  434. };
  435. };
  436. var checkMeta=this.rm(mp3Buffers,length);
  437. if(!checkMeta){
  438. //还原变更,应该不太可能会出现
  439. arr0&&(mp3Buffers[0]=arr0);
  440. for(var i=0;i<arrs.length;i++){
  441. mp3Buffers.splice(i,0,arrs[i]);
  442. };
  443. meta.err="mp3 fix error: 已还原,错误原因不明"; //worker里面没$T翻译
  444. };
  445. var fix=meta.trimFix={};
  446. fix.remove=num;
  447. fix.removeDuration=Math.round(num*meta.frameDurationFloat);
  448. fix.duration=Math.round(length*8/meta.bitRate);
  449. };
  450. return meta;
  451. }
  452. };
  453. var mp3TrimFixSetMeta=function(meta,set){
  454. var tag="MP3 Info: ";
  455. if(meta.sampleRate&&meta.sampleRate!=set.sampleRate || meta.bitRate&&meta.bitRate!=set.bitRate){
  456. Recorder.CLog(tag+$T("uY9i::和设置的不匹配{1},已更新成{2}",0,"set:"+set.bitRate+"kbps "+set.sampleRate+"hz","set:"+meta.bitRate+"kbps "+meta.sampleRate+"hz"),3,set);
  457. set.sampleRate=meta.sampleRate;
  458. set.bitRate=meta.bitRate;
  459. };
  460. var trimFix=meta.trimFix;
  461. if(trimFix){
  462. tag+=$T("iMSm::Fix移除{1}帧",0,trimFix.remove)+" "+trimFix.removeDuration+"ms -> "+trimFix.duration+"ms";
  463. if(trimFix.remove>2){
  464. meta.err=(meta.err?meta.err+", ":"")+$T("b9zm::移除帧数过多");
  465. };
  466. }else{
  467. tag+=(meta.duration||"-")+"ms";
  468. };
  469. if(meta.err){
  470. Recorder.CLog(tag,meta.size?1:0,meta.err,meta);
  471. }else{
  472. Recorder.CLog(tag,meta);
  473. };
  474. };
  475. }));