MockingBird/web/static/js/mp3.js

424 lines
12 KiB
JavaScript
Raw Normal View History

/*
mp3编码器需带上mp3-engine.js引擎使用
https://github.com/xiangyuecn/Recorder
当然最佳推荐使用mp3wav格式代码也是优先照顾这两种格式
浏览器支持情况
https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
*/
(function(){
"use strict";
Recorder.prototype.enc_mp3={
stable:true
,testmsg:"采样率范围48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000"
};
//*******标准UI线程转码支持函数************
Recorder.prototype.mp3=function(res,True,False){
var This=this,set=This.set,size=res.length;
//优先采用worker编码太低版本下面用老方法提供兼容
var ctx=This.mp3_start(set);
if(ctx){
This.mp3_encode(ctx,res);
This.mp3_complete(ctx,True,False,1);
return;
};
//https://github.com/wangpengfei15975/recorder.js
//https://github.com/zhuker/lamejs bug:采样率必须和源一致不然8k时没有声音有问题fixhttps://github.com/zhuker/lamejs/pull/11
var mp3=new Recorder.lamejs.Mp3Encoder(1,set.sampleRate,set.bitRate);
var blockSize=57600;
var data=[];
var idx=0,mp3Size=0;
var run=function(){
if(idx<size){
var buf=mp3.encodeBuffer(res.subarray(idx,idx+blockSize));
if(buf.length>0){
mp3Size+=buf.buffer.byteLength;
data.push(buf.buffer);
};
idx+=blockSize;
setTimeout(run);//尽量避免卡ui
}else{
var buf=mp3.flush();
if(buf.length>0){
mp3Size+=buf.buffer.byteLength;
data.push(buf.buffer);
};
//去掉开头的标记信息帧
var meta=mp3TrimFix.fn(data,mp3Size,size,set.sampleRate);
mp3TrimFixSetMeta(meta,set);
True(new Blob(data,{type:"audio/mp3"}));
};
};
run();
}
//********边录边转码(Worker)支持函数,如果提供就代表可能支持,否则只支持标准转码*********
//全局共享一个Worker后台串行执行。如果每次都开一个新的编码速度可能会慢很多可能是浏览器运行缓存的因素并且可能瞬间产生多个并行操作占用大量cpu
var mp3Worker;
Recorder.BindDestroy("mp3Worker",function(){
console.log("mp3Worker Destroy");
mp3Worker&&mp3Worker.terminate();
mp3Worker=null;
});
Recorder.prototype.mp3_envCheck=function(envInfo,set){//检查环境下配置是否可用
var errMsg="";
//需要实时编码返回数据,此时需要检查环境是否有实时特性、和是否可实时编码
if(set.takeoffEncodeChunk){
if(!envInfo.canProcess){
errMsg=envInfo.envName+"环境不支持实时处理";
}else if(!newContext()){//浏览器不能创建实时编码环境
errMsg="当前浏览器版本太低,无法实时处理";
};
};
return errMsg;
};
Recorder.prototype.mp3_start=function(set){//如果返回null代表不支持
return newContext(set);
};
var openList={id:0};
var newContext=function(setOrNull){
var worker=mp3Worker;
try{
if(!worker){
var onmsg=function(e){
var ed=e.data;
var cur=wk_ctxs[ed.id];
if(ed.action=="init"){
wk_ctxs[ed.id]={
sampleRate:ed.sampleRate
,bitRate:ed.bitRate
,takeoff:ed.takeoff
,mp3Size:0
,pcmSize:0
,encArr:[]
,encObj:new wk_lame.Mp3Encoder(1,ed.sampleRate,ed.bitRate)
};
}else if(!cur){
return;
};
switch(ed.action){
case "stop":
cur.encObj=null;
delete wk_ctxs[ed.id];
break;
case "encode":
cur.pcmSize+=ed.pcm.length;
var buf=cur.encObj.encodeBuffer(ed.pcm);
if(buf.length>0){
if(cur.takeoff){
self.postMessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
cur.mp3Size+=buf.buffer.byteLength;
cur.encArr.push(buf.buffer);
};
};
break;
case "complete":
var buf=cur.encObj.flush();
if(buf.length>0){
if(cur.takeoff){
self.postMessage({action:"takeoff",id:ed.id,chunk:buf});
}else{
cur.mp3Size+=buf.buffer.byteLength;
cur.encArr.push(buf.buffer);
};
};
//去掉开头的标记信息帧
var meta=wk_mp3TrimFix.fn(cur.encArr,cur.mp3Size,cur.pcmSize,cur.sampleRate);
self.postMessage({
action:ed.action
,id:ed.id
,blob:new Blob(cur.encArr,{type:"audio/mp3"})
,meta:meta
});
break;
};
};
//创建一个新Worker
var jsCode=");wk_lame();var wk_ctxs={};self.onmessage="+onmsg;
jsCode+=";var wk_mp3TrimFix={rm:"+mp3TrimFix.rm+",fn:"+mp3TrimFix.fn+"}";
var lamejsCode=Recorder.lamejs.toString();
var url=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_lame=(",lamejsCode,jsCode], {type:"text/javascript"}));
worker=new Worker(url);
setTimeout(function(){
(window.URL||webkitURL).revokeObjectURL(url);//必须要释放,不然每次调用内存都明显泄露内存
},10000);//chrome 83 file协议下如果直接释放将会使WebWorker无法启动
worker.onmessage=function(e){
var data=e.data;
var ctx=openList[data.id];
if(ctx){
if(data.action=="takeoff"){
//取走实时生成的mp3数据
ctx.set.takeoffEncodeChunk(new Uint8Array(data.chunk.buffer));
}else{
//complete
ctx.call&&ctx.call(data);
ctx.call=null;
};
};
};
};
var ctx={worker:worker,set:setOrNull,takeoffQueue:[]};
if(setOrNull){
ctx.id=++openList.id;
openList[ctx.id]=ctx;
worker.postMessage({
action:"init"
,id:ctx.id
,sampleRate:setOrNull.sampleRate
,bitRate:setOrNull.bitRate
,takeoff:!!setOrNull.takeoffEncodeChunk
,x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
}else{
worker.postMessage({
x:new Int16Array(5)//低版本浏览器不支持序列化TypedArray
});
};
mp3Worker=worker;
return ctx;
}catch(e){//出错了就不要提供了
worker&&worker.terminate();
console.error(e);
return null;
};
};
Recorder.prototype.mp3_stop=function(startCtx){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"stop"
,id:startCtx.id
});
startCtx.worker=null;
delete openList[startCtx.id];
//疑似泄露检测 排除id
var opens=-1;
for(var k in openList){
opens++;
};
if(opens){
console.warn("mp3 worker剩"+opens+"个在串行等待");
};
};
};
Recorder.prototype.mp3_encode=function(startCtx,pcm){
if(startCtx&&startCtx.worker){
startCtx.worker.postMessage({
action:"encode"
,id:startCtx.id
,pcm:pcm
});
};
};
Recorder.prototype.mp3_complete=function(startCtx,True,False,autoStop){
var This=this;
if(startCtx&&startCtx.worker){
startCtx.call=function(data){
mp3TrimFixSetMeta(data.meta,startCtx.set);
True(data.blob);
if(autoStop){
This.mp3_stop(startCtx);
};
};
startCtx.worker.postMessage({
action:"complete"
,id:startCtx.id
});
}else{
False("mp3编码器未打开");
};
};
//*******辅助函数************
/*lamejsmp3null
mp3Buffers=[ArrayBuffer,...]
length=mp3Buffers的数据二进制总长度
*/
Recorder.mp3ReadMeta=function(mp3Buffers,length){
//kill babel-polyfill ES6 Number.parseInt 不然放到Worker里面找不到方法
var parseInt_ES3=typeof(window)=="object"?window.parseInt:self.parseInt;
var u8arr0=new Uint8Array(mp3Buffers[0]||[]);
if(u8arr0.length<4){
return null;
};
var byteAt=function(idx,u8){
return ("0000000"+((u8||u8arr0)[idx]||0).toString(2)).substr(-8);
};
var b2=byteAt(0)+byteAt(1);
var b4=byteAt(2)+byteAt(3);
if(!/^1{11}/.test(b2)){//未发现帧同步
return null;
};
var version=({"00":2.5,"10":2,"11":1})[b2.substr(11,2)];
var layer=({"01":3})[b2.substr(13,2)];//仅支持Layer3
var sampleRate=({ //lamejs -> Tables.samplerate_table
"1":[44100, 48000, 32000]
,"2":[22050, 24000, 16000]
,"2.5":[11025, 12000, 8000]
})[version];
sampleRate&&(sampleRate=sampleRate[parseInt_ES3(b4.substr(4,2),2)]);
var bitRate=[ //lamejs -> Tables.bitrate_table
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160] //MPEG 2 2.5
,[0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320]//MPEG 1
][version==1?1:0][parseInt_ES3(b4.substr(0,4),2)];
if(!version || !layer || !bitRate || !sampleRate){
return null;
};
var duration=Math.round(length*8/bitRate);
var frame=layer==1?384:layer==2?1152:version==1?1152:576;
var frameDurationFloat=frame/sampleRate*1000;
var frameSize=Math.floor((frame*bitRate)/8/sampleRate*1000);
//检测是否存在Layer3帧填充1字节。这里只获取第二帧的填充信息首帧永远没有填充。其他帧可能隔一帧出现一个填充或者隔很多帧出现一个填充目测是取决于frameSize未舍入时的小数部分因为有些采样率的frameSize会出现小数11025、22050、44100 典型的除不尽),然后字节数无法表示这种小数,就通过一定步长来填充弥补小数部分丢失
var hasPadding=0,seek=0;
for(var i=0;i<mp3Buffers.length;i++){
//寻找第二帧
var buf=mp3Buffers[i];
seek+=buf.byteLength;
if(seek>=frameSize+3){
var buf8=new Uint8Array(buf);
var idx=buf.byteLength-(seek-(frameSize+3)+1);
var ib4=byteAt(idx,buf8);
hasPadding=ib4.charAt(6)=="1";
break;
};
};
if(hasPadding){
frameSize++;
};
return {
version:version //1 2 2.5 -> MPEG1 MPEG2 MPEG2.5
,layer:layer//3 -> Layer3
,sampleRate:sampleRate //采样率 hz
,bitRate:bitRate //比特率 kbps
,duration:duration //音频时长 ms
,size:length //总长度 byte
,hasPadding:hasPadding //是否存在1字节填充首帧永远没有这个值其实代表的第二帧是否有填充并不代表其他帧的
,frameSize:frameSize //每帧最大长度含可能存在的1字节padding byte
,frameDurationFloat:frameDurationFloat //每帧时长,含小数 ms
};
};
//去掉lamejs开头的标记信息帧免得mp3解码出来的时长比pcm的长太多
var mp3TrimFix={//minfiy keep name
rm:Recorder.mp3ReadMeta
,fn:function(mp3Buffers,length,pcmLength,pcmSampleRate){
var meta=this.rm(mp3Buffers,length);
if(!meta){
return {err:"mp3非预定格式"};
};
var pcmDuration=Math.round(pcmLength/pcmSampleRate*1000);
//开头多出这么多帧移除掉正常情况下最多为2帧
var num=Math.floor((meta.duration-pcmDuration)/meta.frameDurationFloat);
if(num>0){
var size=num*meta.frameSize-(meta.hasPadding?1:0);//首帧没有填充第二帧可能有填充这里假设最多为2帧测试并未出现3帧以上情况其他帧不管就算出现了并且导致了错误后面自动容错
length-=size;
var arr0=0,arrs=[];
for(var i=0;i<mp3Buffers.length;i++){
var arr=mp3Buffers[i];
if(size<=0){
break;
};
if(size>=arr.byteLength){
size-=arr.byteLength;
arrs.push(arr);
mp3Buffers.splice(i,1);
i--;
}else{
mp3Buffers[i]=arr.slice(size);
arr0=arr;
size=0;
};
};
var checkMeta=this.rm(mp3Buffers,length);
if(!checkMeta){
//还原变更,应该不太可能会出现
arr0&&(mp3Buffers[0]=arr0);
for(var i=0;i<arrs.length;i++){
mp3Buffers.splice(i,0,arrs[i]);
};
meta.err="fix后数据错误已还原错误原因不明";
};
var fix=meta.trimFix={};
fix.remove=num;
fix.removeDuration=Math.round(num*meta.frameDurationFloat);
fix.duration=Math.round(length*8/meta.bitRate);
};
return meta;
}
};
var mp3TrimFixSetMeta=function(meta,set){
var tag="MP3信息 ";
if(meta.sampleRate&&meta.sampleRate!=set.sampleRate || meta.bitRate&&meta.bitRate!=set.bitRate){
console.warn(tag+"和设置的不匹配set:"+set.bitRate+"kbps "+set.sampleRate+"hz已更新set:"+meta.bitRate+"kbps "+meta.sampleRate+"hz",set);
set.sampleRate=meta.sampleRate;
set.bitRate=meta.bitRate;
};
var trimFix=meta.trimFix;
if(trimFix){
tag+="Fix移除"+trimFix.remove+"帧"+trimFix.removeDuration+"ms -> "+trimFix.duration+"ms";
if(trimFix.remove>2){
meta.err=(meta.err?meta.err+", ":"")+"移除帧数过多";
};
}else{
tag+=(meta.duration||"-")+"ms";
};
if(meta.err){
console.error(tag,meta.err,meta);
}else{
console.log(tag,meta);
};
};
})();