generated from template/vite-react-template
887 lines
33 KiB
JavaScript
887 lines
33 KiB
JavaScript
/*
|
||
录音 Recorder扩展,实时播放录音片段文件,把片段文件转换成MediaStream流
|
||
|
||
https://github.com/xiangyuecn/Recorder
|
||
|
||
BufferStreamPlayer可以通过input方法一次性输入整个音频文件,或者实时输入音频片段文件,然后播放出来;输入支持格式:pcm、wav、mp3等浏览器支持的音频格式,非pcm格式会自动解码成pcm(播放音质效果比pcm、wav格式差点);输入前输入后都可进行处理要播放的音频,比如:混音、变速、变调;输入的音频会写入到内部的MediaStream流中,完成将连续的音频片段文件转换成流。
|
||
|
||
BufferStreamPlayer可以用于:
|
||
1. Recorder onProcess等实时处理中,将实时处理好的音频片段转直接换成MediaStream,此流可以作为WebRTC的local流发送到对方,或播放出来;
|
||
2. 接收到的音频片段文件的实时播放,比如:WebSocket接收到的录音片段文件播放、WebRTC remote流(Recorder支持对这种流进行实时处理)实时处理后的播放;
|
||
3. 单个音频文件的实时播放处理,比如:播放一段音频,并同时进行可视化绘制(其实自己解码+播放绘制比直接调用这个更有趣,但这个省事、配套功能多点)。
|
||
|
||
在线测试例子:
|
||
https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.decode_buffer_stream_player
|
||
调用示例:
|
||
var stream=Recorder.BufferStreamPlayer(set)
|
||
//创建好后第一件事就是start打开流,打开后就会开始播放input输入的音频,set具体配置看下面源码;注意:start需要在用户操作(触摸、点击等)时进行调用,原因参考runningContext配置
|
||
stream.start(()=>{
|
||
stream.currentTime;//当前已播放的时长,单位ms,数值变化时会有onUpdateTime事件
|
||
stream.duration;//已输入的全部数据总时长,单位ms,数值变化时会有onUpdateTime事件;实时模式下意义不大,会比实际播放的长,因为实时播放时卡了就会丢弃部分数据不播放
|
||
stream.isStop;//是否已停止,调用了stop方法时会设为true
|
||
stream.isPause;//是否已暂停,调用了pause方法时会设为true
|
||
stream.isPlayEnd;//已输入的数据是否播放到了结尾(没有可播放的数据了),input后又会变成false;可代表正在缓冲中或播放结束,状态变更时会有onPlayEnd事件
|
||
|
||
//如果不要默认的播放,可以设置set.play为false,这种情况下只拿到MediaStream来用
|
||
stream.getMediaStream() //通过getMediaStream方法得到MediaStream流,此流可以作为WebRTC的local流发送到对方,或者直接拿来赋值给audio.srcObject来播放(和赋值audio.src作用一致);未start时调用此方法将会抛异常
|
||
|
||
stream.getAudioSrc() //【已过时】超低版本浏览器中得到MediaStream流的字符串播放地址,可赋值给audio标签的src,直接播放音频;未start时调用此方法将会抛异常;新版本浏览器已停止支持将MediaStream转换成url字符串,调用本方法新浏览器会抛异常,因此在不需要兼容不支持srcObject的超低版本浏览器时,请直接使用getMediaStream然后赋值给auido.srcObject来播放
|
||
},(errMsg)=>{
|
||
//start失败,无法播放
|
||
});
|
||
|
||
//随时都能调用input,会等到start成功后播放出来,不停的调用input,就能持续的播放出声音了,需要暂停播放就不要调用input就行了
|
||
stream.input(anyData); //anyData数据格式 和更多说明,请阅读下面的input方法源码注释
|
||
stream.clearInput(keepDuration); //清除已输入但还未播放的数据,一般用于非实时模式打断老的播放;返回清除的音频时长,默认会从总时长duration中减去此时长,keepDuration=true时不减去
|
||
|
||
//暂停播放,暂停后:实时模式下会丢弃所有input输入的数据(resume时只播放新input的数据),非实时模式下所有input输入的数据会保留到resume时继续播放
|
||
stream.pause();
|
||
//恢复播放,实时模式下只会从最新input的数据开始播放,非实时模式下会从暂停的位置继续播放
|
||
stream.resume();
|
||
|
||
//不要播放了就调用stop停止播放,关闭所有资源
|
||
stream.stop();
|
||
|
||
|
||
|
||
注意:已知Firefox的AudioBuffer没法动态修改数据,所以对于带有这种特性的浏览器将采用先缓冲后再播放(类似assets/runtime-codes/fragment.playbuffer.js),音质会相对差一点;其他浏览器测试Android、IOS、Chrome无此问题;start方法中有一大段代码给浏览器做了特性检测并进行兼容处理。
|
||
*/
|
||
(function(factory){
|
||
var browser=typeof window=="object" && !!window.document;
|
||
var win=browser?window:Object; //非浏览器环境,Recorder挂载在Object下面
|
||
var rec=win.Recorder,ni=rec.i18n;
|
||
factory(rec,ni,ni.$T,browser);
|
||
}(function(Recorder,i18n,$T,isBrowser){
|
||
"use strict";
|
||
|
||
var BufferStreamPlayer=function(set){
|
||
return new fn(set);
|
||
};
|
||
var BufferStreamPlayerTxt="BufferStreamPlayer";
|
||
var fn=function(set){
|
||
var This=this;
|
||
var o={
|
||
play:true //要播放声音,设为false不播放,只提供MediaStream
|
||
,realtime:true /*默认为true实时模式,设为false为非实时模式
|
||
实时模式:设为 true 或 {maxDelay:300,discardAll:false}配置对象
|
||
如果有新的input输入数据,但之前输入的数据还未播放完的时长不超过maxDelay时(缓冲播放延迟默认限制在300ms内),如果积压的数据量过大则积压的数据将会被直接丢弃,少量积压会和新数据一起加速播放,最终达到尽快播放新输入的数据的目的;这在网络不流畅卡顿时会发挥很大作用,可有效降低播放延迟;出现加速播放时声音听起来会比较怪异,可配置discardAll=true来关闭此特性,少量积压的数据也直接丢弃,不会加速播放;如果你的音频数据块超过200ms,需要调大maxDelay(取值100-800ms)
|
||
非实时模式:设为 false
|
||
连续完整的播放完所有input输入的数据,之前输入的还未播放完又有新input输入会加入队列排队播放,比如用于:一次性同时输入几段音频完整播放
|
||
*/
|
||
|
||
|
||
//,onInputError:fn(errMsg, inputIndex) //当input输入出错时回调,参数为input第几次调用和错误消息
|
||
//,onUpdateTime:fn() //已播放时长、总时长更新回调(stop、pause、resume后一定会回调),this.currentTime为已播放时长,this.duration为已输入的全部数据总时长(实时模式下意义不大,会比实际播放的长),单位都是ms
|
||
//,onPlayEnd:fn() //没有可播放的数据时回调(stop后一定会回调),已输入的数据已全部播放完了,可代表正在缓冲中或播放结束;之后如果继续input输入了新数据,播放完后会再次回调,因此会多次回调;非实时模式一次性输入了数据时,此回调相当于播放完成,可以stop掉,重新创建对象来input数据可达到循环播放效果
|
||
|
||
//,decode:false //input输入的数据在调用transform之前是否要进行一次音频解码成pcm [Int16,...]
|
||
//mp3、wav等都可以设为true、或设为{fadeInOut:true}配置对象,会自动解码成pcm;默认会开启fadeInOut对解码的pcm首尾进行淡入淡出处理,减少爆音(wav等解码后和原始pcm一致的音频,可以把fadeInOut设为false)
|
||
|
||
//transform:fn(inputData,sampleRate,True,False)
|
||
//将input输入的data(如果开启了decode将是解码后的pcm)转换处理成要播放的pcm数据;如果没有解码也没有提供本方法,input的data必须是[Int16,...]并且设置set.sampleRate
|
||
//inputData:any input方法输入的任意格式数据,只要这个转换函数支持处理;如果开启了decode,此数据为input输入的数据解码后的pcm [Int16,...]
|
||
//sampleRate:123 如果设置了decode为解码后的采样率,否则为set.sampleRate || null
|
||
//True(pcm,sampleRate) 回调处理好的pcm数据([Int16,...])和pcm的采样率
|
||
//False(errMsg) 处理失败回调
|
||
|
||
//sampleRate:16000 //可选input输入的数据默认的采样率,当没有设置解码也没有提供transform时应当明确设置采样率
|
||
|
||
//runningContext:AudioContext //可选提供一个state为running状态的AudioContext对象(ctx),默认会在start时自动创建一个新的ctx,这个配置的作用请参阅Recorder的runningContext配置
|
||
};
|
||
for(var k in set){
|
||
o[k]=set[k];
|
||
};
|
||
This.set=set=o;
|
||
|
||
if(!set.onInputError){
|
||
set.onInputError=function(err,n){ CLog(err,1); };
|
||
}
|
||
};
|
||
fn.prototype=BufferStreamPlayer.prototype={
|
||
/**【已过时】获取MediaStream的audio播放地址,新版浏览器、未start将会抛异常**/
|
||
getAudioSrc:function(){
|
||
CLog($T("0XYC::getAudioSrc方法已过时:请直接使用getMediaStream然后赋值给audio.srcObject,仅允许在不支持srcObject的浏览器中调用本方法赋值给audio.src以做兼容"),3);
|
||
if(!this._src){
|
||
//新版chrome调用createObjectURL会直接抛异常了 https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL#using_object_urls_for_media_streams
|
||
this._src=(window.URL||webkitURL).createObjectURL(this.getMediaStream());
|
||
}
|
||
return this._src;
|
||
}
|
||
/**获取MediaStream流对象,未start将会抛异常**/
|
||
,getMediaStream:function(){
|
||
if(!this._dest){
|
||
throw new Error(NoStartMsg());
|
||
}
|
||
return this._dest.stream;
|
||
}
|
||
|
||
|
||
/**打开音频流,打开后就会开始播放input输入的音频;注意:start需要在用户操作(触摸、点击等)时进行调用,原因参考runningContext配置
|
||
* True() 打开成功回调
|
||
* False(errMsg) 打开失败回调**/
|
||
,start:function(True,False){
|
||
var falseCall=function(msg,noClear){
|
||
var next=!checkStop();
|
||
if(!noClear)This._clear();
|
||
CLog(msg,1);
|
||
next&&False&&False(msg);
|
||
};
|
||
var checkStop=function(){
|
||
if(This.isStop){
|
||
CLog($T("6DDt::start被stop终止"),3);
|
||
return true;
|
||
};
|
||
};
|
||
var This=this,set=This.set,__abTest=This.__abTest;
|
||
if(This._Tc!=null){
|
||
falseCall($T("I4h4::{1}多次start",0,BufferStreamPlayerTxt),1);
|
||
return;
|
||
}
|
||
if(!isBrowser){
|
||
falseCall($T.G("NonBrowser-1",[BufferStreamPlayerTxt]));
|
||
return;
|
||
}
|
||
This._Tc=0;//currentTime 对应的采样数
|
||
This._Td=0;//duration 对应的采样数
|
||
|
||
This.currentTime=0;//当前已播放的时长,单位ms
|
||
This.duration=0;//已输入的全部数据总时长,单位ms;实时模式下意义不大,会比实际播放的长,因为实时播放时卡了就会丢弃部分数据不播放
|
||
This.isStop=0;//是否已停止
|
||
This.isPause=0;//是否已暂停
|
||
This.isPlayEnd=0;//已输入的数据是否播放到了结尾(没有可播放的数据了),input后又会变成false;可代表正在缓冲中或播放结束
|
||
|
||
This.inputN=0;//第n次调用input
|
||
|
||
This.inputQueueIdx=0;//input调用队列当前已处理到的位置
|
||
This.inputQueue=[];//input调用队列,用于纠正执行顺序
|
||
|
||
This.bufferSampleRate=0;//audioBuffer的采样率,首次input后就会固定下来
|
||
This.audioBuffer=0;
|
||
This.pcmBuffer=[[],[]];//未推入audioBuffer的pcm数据缓冲
|
||
|
||
var fail=function(msg){
|
||
falseCall($T("P6Gs::浏览器不支持打开{1}",0,BufferStreamPlayerTxt)+(msg?": "+msg:""));
|
||
};
|
||
|
||
var ctx=set.runningContext || Recorder.GetContext(true); This._ctx=ctx;
|
||
var sVal=ctx.state,spEnd=Recorder.CtxSpEnd(sVal);
|
||
!__abTest&&CLog("start... ctx.state="+sVal+(
|
||
spEnd?$T("JwDm::(注意:ctx不是running状态,start需要在用户操作(触摸、点击等)时进行调用,否则会尝试进行ctx.resume,可能会产生兼容性问题(仅iOS),请参阅文档中runningContext配置)"):""
|
||
));
|
||
|
||
var support=1;
|
||
if(!ctx || !ctx.createMediaStreamDestination){
|
||
support=0;
|
||
}else{
|
||
var source=ctx.createBufferSource();
|
||
if(!source.start || source.onended===undefined){
|
||
support=0;//createBufferSource版本太低,难兼容
|
||
}
|
||
};
|
||
if(!support){
|
||
fail("");
|
||
return;
|
||
};
|
||
|
||
|
||
var end=function(){
|
||
if(checkStop())return;
|
||
//创建MediaStream
|
||
var dest=ctx.createMediaStreamDestination();
|
||
dest.channelCount=1;
|
||
This._dest=dest;
|
||
|
||
!__abTest&&CLog("start ok");
|
||
True&&True();
|
||
|
||
This._inputProcess();//处理未完成start前的input调用
|
||
This._updateTime();//更新时间
|
||
|
||
//定时在没有input输入时,将未写入buffer的数据写进去
|
||
if(!badAB){
|
||
This._writeInt=setInterval(function(){
|
||
This._writeBuffer();
|
||
},100);
|
||
}else{
|
||
CLog($T("qx6X::此浏览器的AudioBuffer实现不支持动态特性,采用兼容模式"),3);
|
||
This._writeInt=setInterval(function(){
|
||
This._writeBad();
|
||
},10);//定时调用进行数据写入播放
|
||
}
|
||
};
|
||
var abTest=function(){
|
||
//浏览器实现检测,已知Firefox的AudioBuffer没法在_writeBuffer中动态修改数据;检测方法:直接新开一个,输入一段测试数据,看看能不能拿到流中的数据
|
||
var testStream=BufferStreamPlayer({ play:false,sampleRate:8000,runningContext:ctx });
|
||
testStream.__abTest=1; var testRec;
|
||
testStream.start(function(){
|
||
testRec=Recorder({
|
||
type:"unknown"
|
||
,sourceStream:testStream.getMediaStream()
|
||
,runningContext:ctx
|
||
,onProcess:function(buffers){
|
||
var bf=buffers[buffers.length-1],all0=1;
|
||
for(var i=0;i<bf.length;i++){
|
||
if(bf[i]!=0){ all0=0; break; }
|
||
}
|
||
if(all0 && buffers.length<5){
|
||
return;//再等等看,最长约等500ms
|
||
}
|
||
testRec.close();
|
||
testStream.stop();
|
||
|
||
if(testInt){ clearTimeout(testInt); testInt=0;
|
||
//全部是0就是浏览器不行,要缓冲一次性播放进行兼容
|
||
badAB=all0;
|
||
BufferStreamPlayer.BadAudioBuffer=badAB;
|
||
end();
|
||
}
|
||
}
|
||
});
|
||
testRec.open(function(){
|
||
testRec.start();
|
||
},function(msg){
|
||
testStream.stop(); fail(msg);
|
||
});
|
||
},fail);
|
||
//超时没有回调
|
||
var testInt=setTimeout(function(){
|
||
testInt=0; testStream.stop(); testRec&&testRec.close();
|
||
fail($T("cdOx::环境检测超时"));
|
||
},1500);
|
||
//随机生成1秒的数据,rec有一次回调即可
|
||
var data=new Int16Array(8000);
|
||
for(var i=0;i<8000;i++){
|
||
data[i]=~~(Math.random()*0x7fff*2-0x7fff);
|
||
}
|
||
testStream.input(data);
|
||
};
|
||
|
||
var badAB=BufferStreamPlayer.BadAudioBuffer;
|
||
var ctxNext=function(){
|
||
if(__abTest || badAB!=null){
|
||
setTimeout(end); //应当setTimeout一下强转成异步,统一调用代码时的行为
|
||
}else{
|
||
abTest();
|
||
};
|
||
};
|
||
var tag="AudioContext resume: ";
|
||
Recorder.ResumeCtx(ctx,function(runC){
|
||
runC&&CLog(tag+"wait...");
|
||
return !This.isStop;
|
||
},function(runC){
|
||
runC&&CLog(tag+ctx.state);
|
||
ctxNext();
|
||
},function(err){ //比较少见,可能没有影响
|
||
CLog(tag+ctx.state+" "+$T("S2Bu::可能无法播放:{1}",0,err),1);
|
||
ctxNext();
|
||
});
|
||
}
|
||
,_clear:function(){
|
||
var This=this;
|
||
This.isStop=1;
|
||
clearInterval(This._writeInt);
|
||
This.inputQueue=0;
|
||
|
||
if(This._src){
|
||
(window.URL||webkitURL).revokeObjectURL(This._src);
|
||
This._src=0;
|
||
}
|
||
if(This._dest){
|
||
Recorder.StopS_(This._dest.stream);
|
||
This._dest=0;
|
||
}
|
||
if(!This.set.runningContext && This._ctx){
|
||
Recorder.CloseNewCtx(This._ctx);
|
||
}
|
||
This._ctx=0;
|
||
|
||
var source=This.bufferSource;
|
||
if(source){
|
||
source.disconnect();
|
||
source.stop();
|
||
}
|
||
This.bufferSource=0;
|
||
This.audioBuffer=0;
|
||
}
|
||
/**停止播放,关闭所有资源**/
|
||
,stop:function(){
|
||
var This=this;
|
||
This._clear();
|
||
|
||
!This.__abTest&&CLog("stop");
|
||
This._playEnd(1);
|
||
}
|
||
/**暂停播放,暂停后:实时模式下会丢弃所有input输入的数据(resume时只播放新input的数据),非实时模式下所有input输入的数据会保留到resume时继续播放**/
|
||
,pause:function(){
|
||
CLog("pause");
|
||
this.isPause=1;
|
||
this._updateTime(1);
|
||
}
|
||
/**恢复播放,实时模式下只会从最新input的数据开始播放,非实时模式下会从暂停的位置继续播放**/
|
||
,resume:function(){
|
||
var This=this,tag="resume",tag3=tag+"(wait ctx)";
|
||
CLog(tag);
|
||
This.isPause=0;
|
||
This._updateTime(1);
|
||
|
||
var ctx=This._ctx;
|
||
if(ctx){ //AudioContext如果被暂停,尽量恢复
|
||
Recorder.ResumeCtx(ctx,function(runC){
|
||
runC&&CLog(tag3+"...");
|
||
return !This.isStop && !This.isPause;
|
||
},function(runC){
|
||
runC&&CLog(tag3+ctx.state);
|
||
},function(err){
|
||
CLog(tag3+ctx.state+"[err]"+err,1);
|
||
});
|
||
};
|
||
}
|
||
|
||
|
||
//当前输入的数据播放到结尾时触发回调,stop时永远会触发回调
|
||
,_playEnd:function(stop){
|
||
var This=this,startTime=This._PNs,call=This.set.onPlayEnd;
|
||
if(stop || !This.isPause){//暂停播到结尾不算
|
||
if(stop || !This.isPlayEnd){
|
||
if(stop || (startTime && Date.now()-startTime>500)){//已停止或者延迟确认成功
|
||
This._PNs=0;
|
||
This.isPlayEnd=1;
|
||
call&&call();
|
||
This._updateTime(1);
|
||
}else if(!startTime){//刚检测到的没有数据了,开始延迟确认
|
||
This._PNs=Date.now();
|
||
};
|
||
};
|
||
};
|
||
}
|
||
//有数据播放时,取消已到结尾状态
|
||
,_playLive:function(){
|
||
var This=this;
|
||
This.isPlayEnd=0;
|
||
This._PNs=0;
|
||
}
|
||
//时间更新时触发回调,没有更新时不会触发回调
|
||
,_updateTime:function(must){
|
||
var This=this,sampleRate=This.bufferSampleRate||9e9,call=This.set.onUpdateTime;
|
||
This.currentTime=Math.round(This._Tc/sampleRate*1000);
|
||
This.duration=Math.round(This._Td/sampleRate*1000);
|
||
|
||
var s=""+This.currentTime+This.duration;
|
||
if(must || This._UTs!=s){
|
||
This._UTs=s;
|
||
call&&call();
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
/**输入任意格式的音频数据,未完成start前调用会等到start成功后生效
|
||
anyData: any 具体类型取决于:
|
||
set.decode为false时:
|
||
未提供set.transform,数据必须是pcm[Int16,...],此时的set必须提供sampleRate;
|
||
提供了set.transform,数据为transform方法支持的任意格式。
|
||
set.decode为true时:
|
||
数据必须是ArrayBuffer,会自动解码成pcm[Int16,...];注意输入的每一片数据都应该是完整的一个音频片段文件,否则可能会解码失败;注意ArrayBuffer对象是Transferable object,参与解码后此对象将不可用,因为内存数据已被转移到了解码线程,可通过 stream.input(arrayBuffer.slice(0)) 形式复制一份再解码就没有这个问题了。
|
||
|
||
关于anyData的二进制长度:
|
||
如果是提供的pcm、wav格式数据,数据长度对播放无太大影响,很短的数据也能很好的连续播放。
|
||
如果是提供的mp3这种必须解码才能获得pcm的数据,数据应当尽量长点,测试发现片段有300ms以上解码后能很好的连续播放,低于100ms解码后可能会有明显的杂音,更低的可能会解码失败;当片段确实太小时,可以将本来会多次input调用的数据缓冲起来,等数据量达到了300ms再来调用一次input,能比较显著的改善播放音质。
|
||
**/
|
||
,input:function(anyData){
|
||
var This=this,set=This.set;
|
||
var inputN=++This.inputN;
|
||
if(!This.inputQueue){
|
||
throw new Error(NoStartMsg());
|
||
}
|
||
|
||
var decSet=set.decode;
|
||
if(decSet){
|
||
//先解码
|
||
DecodeAudio(anyData, function(data){
|
||
if(!This.inputQueue)return;//stop了
|
||
if(decSet.fadeInOut==null || decSet.fadeInOut){
|
||
FadeInOut(data.data, data.sampleRate);//解码后的数据进行一下淡入淡出处理,减少爆音
|
||
}
|
||
This._input2(inputN, data.data, data.sampleRate);
|
||
},function(err){
|
||
This._inputErr(err, inputN);
|
||
});
|
||
}else{
|
||
This._input2(inputN, anyData, set.sampleRate);
|
||
}
|
||
}
|
||
//transform处理
|
||
,_input2:function(inputN, anyData, sampleRate){
|
||
var This=this,set=This.set;
|
||
|
||
if(set.transform){
|
||
set.transform(anyData, sampleRate, function(pcm, sampleRate2){
|
||
if(!This.inputQueue)return;//stop了
|
||
|
||
sampleRate=sampleRate2||sampleRate;
|
||
This._input3(inputN, pcm, sampleRate);
|
||
},function(err){
|
||
This._inputErr(err, inputN);
|
||
});
|
||
}else{
|
||
This._input3(inputN, anyData, sampleRate);
|
||
}
|
||
}
|
||
//转换好的pcm加入input队列,纠正调用顺序,未start时等待
|
||
,_input3:function(inputN, pcm, sampleRate){
|
||
var This=this;
|
||
|
||
if(!pcm || !pcm.subarray){
|
||
This._inputErr($T("ZfGG::input调用失败:非pcm[Int16,...]输入时,必须解码或者使用transform转换"), inputN);
|
||
return;
|
||
}
|
||
if(!sampleRate){
|
||
This._inputErr($T("N4ke::input调用失败:未提供sampleRate"), inputN);
|
||
return;
|
||
}
|
||
if(This.bufferSampleRate && This.bufferSampleRate!=sampleRate){
|
||
This._inputErr($T("IHZd::input调用失败:data的sampleRate={1}和之前的={2}不同",0,sampleRate,This.bufferSampleRate), inputN);
|
||
return;
|
||
}
|
||
if(!This.bufferSampleRate){
|
||
This.bufferSampleRate=sampleRate;//首次处理后,固定下来,后续的每次输入都是相同的
|
||
}
|
||
|
||
//加入队列,纠正input执行顺序,解码、transform均有可能会导致顺序不一致
|
||
if(inputN>This.inputQueueIdx){ //clearInput移动了队列位置的丢弃
|
||
This.inputQueue[inputN]=pcm;
|
||
}
|
||
|
||
if(This._dest){//已start,可以开始处理队列
|
||
This._inputProcess();
|
||
}
|
||
}
|
||
,_inputErr:function(errMsg, inputN){
|
||
if(!this.inputQueue) return;//stop了
|
||
this.inputQueue[inputN]=1;//出错了,队列里面也要占个位
|
||
this.set.onInputError(errMsg, inputN);
|
||
}
|
||
//处理input队列
|
||
,_inputProcess:function(){
|
||
var This=this;
|
||
if(!This.bufferSampleRate){
|
||
return;
|
||
}
|
||
|
||
var queue=This.inputQueue;
|
||
for(var i=This.inputQueueIdx+1;i<queue.length;i++){ //inputN是从1开始,所以+1
|
||
var pcm=queue[i];
|
||
if(pcm==1){
|
||
This.inputQueueIdx=i;//跳过出错的input
|
||
continue;
|
||
}
|
||
if(!pcm){
|
||
return;//之前的input还未进入本方法,退出等待
|
||
}
|
||
|
||
This.inputQueueIdx=i;
|
||
queue[i]=null;
|
||
|
||
//推入缓冲,最多两个元素 [堆积的,新的]
|
||
var pcms=This.pcmBuffer;
|
||
var pcm0=pcms[0],pcm1=pcms[1];
|
||
if(pcm0.length){
|
||
if(pcm1.length){
|
||
var tmp=new Int16Array(pcm0.length+pcm1.length);
|
||
tmp.set(pcm0);
|
||
tmp.set(pcm1,pcm0.length);
|
||
pcms[0]=tmp;
|
||
}
|
||
}else{
|
||
pcms[0]=pcm1;
|
||
}
|
||
pcms[1]=pcm;
|
||
|
||
This._Td+=pcm.length;//更新已输入总时长
|
||
This._updateTime();
|
||
This._playLive();//有播放数据了
|
||
}
|
||
|
||
if(!BufferStreamPlayer.BadAudioBuffer){
|
||
if(!This.audioBuffer){
|
||
This._createBuffer(true);
|
||
}else{
|
||
This._writeBuffer();
|
||
}
|
||
}else{
|
||
This._writeBad();
|
||
}
|
||
}
|
||
|
||
/**清除已输入但还未播放的数据,一般用于非实时模式打断老的播放;返回清除的音频时长,默认会从总时长duration中减去此时长,keepDuration时不减去*/
|
||
,clearInput:function(keepDuration){
|
||
var This=this, sampleRate=This.bufferSampleRate, size=0;
|
||
if(This.inputQueue){//未stop
|
||
This.inputQueueIdx=This.inputN;//队列位置移到结尾
|
||
|
||
var pcms=This.pcmBuffer;
|
||
size=pcms[0].length+pcms[1].length;
|
||
This._subClear();
|
||
if(!keepDuration) This._Td-=size;//减掉已输入总时长
|
||
This._updateTime(1);
|
||
}
|
||
var dur = size? Math.round(size/sampleRate*1000) : 0;
|
||
CLog("clearInput "+dur+"ms "+size);
|
||
return dur;
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
/****************正常的播放处理****************/
|
||
//创建播放buffer
|
||
,_createBuffer:function(init){
|
||
var This=this,set=This.set;
|
||
if(!init && !This.audioBuffer){
|
||
return;
|
||
}
|
||
|
||
var ctx=This._ctx;
|
||
var sampleRate=This.bufferSampleRate;
|
||
var bufferSize=sampleRate*(set.bufferSecond||60);//建一个可以持续播放60秒的buffer,循环写入数据播放,大点好简单省事
|
||
var buffer=ctx.createBuffer(1, bufferSize,sampleRate);
|
||
|
||
var source=ctx.createBufferSource();
|
||
source.channelCount=1;
|
||
source.buffer=buffer;
|
||
source.connect(This._dest);
|
||
if(set.play){//播放出声音
|
||
source.connect(ctx.destination);
|
||
}
|
||
source.onended=function(){
|
||
source.disconnect();
|
||
source.stop();
|
||
|
||
This._createBuffer();//重新创建buffer
|
||
};
|
||
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
|
||
|
||
This.bufferSource=source;
|
||
This.audioBuffer=buffer;
|
||
This.audioBufferIdx=0;
|
||
This._createBufferTime=Date.now();
|
||
|
||
This._writeBuffer();
|
||
}
|
||
,_writeBuffer:function(){
|
||
var This=this,set=This.set;
|
||
var buffer=This.audioBuffer;
|
||
var sampleRate=This.bufferSampleRate;
|
||
var oldAudioBufferIdx=This.audioBufferIdx;
|
||
if(!buffer){
|
||
return;
|
||
}
|
||
|
||
//计算已播放的量,可能已播放过头了,卡了没有数据
|
||
var playSize=Math.floor((Date.now()-This._createBufferTime)/1000*sampleRate);
|
||
if(This.audioBufferIdx+0.005*sampleRate<playSize){//5ms动态区间
|
||
This.audioBufferIdx=playSize;//将写入位置修正到当前播放位置
|
||
}
|
||
//写进去了,但还未被播放的量
|
||
var wnSize=Math.max(0, This.audioBufferIdx-playSize);
|
||
|
||
//这次最大能写入多少;限制到800ms,包括写入了还未播放的
|
||
var maxSize=buffer.length-This.audioBufferIdx;
|
||
maxSize=Math.min(maxSize, ~~(0.8*sampleRate)-wnSize);
|
||
if(maxSize<1){//写不下了,退出
|
||
return;
|
||
}
|
||
|
||
if(This._subPause()){//暂停了,不消费缓冲数据
|
||
return;
|
||
};
|
||
var pcms=This.pcmBuffer;
|
||
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
|
||
if(pcm0.length+pcm1Len==0){//无可用数据,退出
|
||
This._playEnd();//无可播放数据回调
|
||
return;
|
||
};
|
||
This._playLive();//有播放数据了
|
||
|
||
var pcmSize=0,speed=1;
|
||
var realMode=set.realtime;
|
||
while(realMode){
|
||
//************实时模式************
|
||
//尽量同步播放,避免过大延迟,但始终保持延迟150ms播放新数据,这样每次添加进新数据都是接到还未播放到的最后面,减少引入的杂音,减少网络波动的影响
|
||
var delaySecond=0.15;
|
||
|
||
//计算当前堆积的量
|
||
var dSize=wnSize+pcm0.length;
|
||
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
|
||
|
||
//堆积的在300ms内按正常播放
|
||
if(dSize<dMax){
|
||
//至少要延迟播放新数据
|
||
var d150Size=Math.floor(delaySecond*sampleRate-dSize-pcm1Len);
|
||
if(oldAudioBufferIdx==0 && d150Size>0){
|
||
//开头加上少了的延迟
|
||
This.audioBufferIdx=Math.max(This.audioBufferIdx, d150Size);
|
||
}
|
||
|
||
realMode=false;//切换成顺序播放
|
||
break;
|
||
}
|
||
//堆积的太多,配置为全丢弃
|
||
if(realMode.discardAll){
|
||
if(dSize>dMax*1.333){//超过400ms,取200ms正常播放,300ms中位数
|
||
pcm0=This._cutPcm0(Math.round(dMax*0.666-wnSize-pcm1Len));
|
||
}
|
||
realMode=false;//切换成顺序播放
|
||
break;
|
||
}
|
||
|
||
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
|
||
pcm0=This._cutPcm0(3*sampleRate-wnSize-pcm1Len);
|
||
|
||
speed=1.6;//倍速,重采样
|
||
//计算要截取出来量
|
||
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
|
||
break;
|
||
}
|
||
if(!realMode){
|
||
//*******按顺序取数据播放*********
|
||
//计算要截取出来量
|
||
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
|
||
}
|
||
if(!pcmSize){
|
||
return;
|
||
}
|
||
|
||
//截取数据并写入到audioBuffer中
|
||
This.audioBufferIdx=This._subWrite(buffer,pcmSize,This.audioBufferIdx,speed);
|
||
}
|
||
|
||
|
||
/****************兼容播放处理,播放音质略微差点****************/
|
||
,_writeBad:function(){
|
||
var This=this,set=This.set;
|
||
var buffer=This.audioBuffer;
|
||
var sampleRate=This.bufferSampleRate;
|
||
var ctx=This._ctx;
|
||
|
||
//正在播放,5ms不能结束就等待播放完,定时器是10ms
|
||
if(buffer){
|
||
var ms=buffer.length/sampleRate*1000;
|
||
if(Date.now()-This._createBufferTime<ms-5){
|
||
return;
|
||
}
|
||
}
|
||
|
||
//这次最大能写入多少;限制到800ms
|
||
var maxSize=~~(0.8*sampleRate);
|
||
var st=set.PlayBufferDisable?0:sampleRate/1000*300;//缓冲播放,不然间隔太短接续爆音明显
|
||
|
||
if(This._subPause()){//暂停了,不消费缓冲数据
|
||
return;
|
||
};
|
||
var pcms=This.pcmBuffer;
|
||
var pcm0=pcms[0],pcm1=pcms[1],pcm1Len=pcm1.length;
|
||
var allSize=pcm0.length+pcm1Len;
|
||
if(allSize==0 || allSize<st){//无可用数据 不够缓冲量,退出
|
||
This._playEnd();//无可播放数据回调,最后一丁点会始终等缓冲满导致卡住
|
||
return;
|
||
};
|
||
This._playLive();//有播放数据了
|
||
|
||
var pcmSize=0,speed=1;
|
||
var realMode=set.realtime;
|
||
while(realMode){
|
||
//************实时模式************
|
||
//计算当前堆积的量
|
||
var dSize=pcm0.length;
|
||
var dMax=(realMode.maxDelay||300)/1000 *sampleRate;
|
||
|
||
//堆积的在300ms内按正常播放
|
||
if(dSize<dMax){
|
||
realMode=false;//切换成顺序播放
|
||
break;
|
||
}
|
||
//堆积的太多,配置为全丢弃
|
||
if(realMode.discardAll){
|
||
if(dSize>dMax*1.333){//超过400ms,取200ms正常播放,300ms中位数
|
||
pcm0=This._cutPcm0(Math.round(dMax*0.666-pcm1Len));
|
||
}
|
||
realMode=false;//切换成顺序播放
|
||
break;
|
||
}
|
||
|
||
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
|
||
pcm0=This._cutPcm0(3*sampleRate-pcm1Len);
|
||
|
||
speed=1.6;//倍速,重采样
|
||
//计算要截取出来量
|
||
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1Len)/speed));
|
||
break;
|
||
}
|
||
if(!realMode){
|
||
//*******按顺序取数据播放*********
|
||
//计算要截取出来量
|
||
pcmSize=Math.min(maxSize, pcm0.length+pcm1Len);
|
||
}
|
||
if(!pcmSize){
|
||
return;
|
||
}
|
||
|
||
//新建buffer,一次性完整播放当前的数据
|
||
buffer=ctx.createBuffer(1,pcmSize,sampleRate);
|
||
|
||
//截取数据并写入到audioBuffer中
|
||
This._subWrite(buffer,pcmSize,0,speed);
|
||
|
||
//首尾进行1ms的淡入淡出 大幅减弱爆音
|
||
FadeInOut(buffer.getChannelData(0), sampleRate);
|
||
|
||
var source=ctx.createBufferSource();
|
||
source.channelCount=1;
|
||
source.buffer=buffer;
|
||
source.connect(This._dest);
|
||
if(set.play){//播放出声音
|
||
source.connect(ctx.destination);
|
||
}
|
||
source.start();//古董 source.noteOn(0) 不支持onended 放弃支持
|
||
|
||
This.bufferSource=source;
|
||
This.audioBuffer=buffer;
|
||
This._createBufferTime=Date.now();
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
,_cutPcm0:function(pcmNs){//保留堆积的数据到指定的时长数量
|
||
var pcms=this.pcmBuffer,pcm0=pcms[0];
|
||
if(pcmNs<0)pcmNs=0;
|
||
if(pcm0.length>pcmNs){//丢弃超过秒数的
|
||
var size=pcm0.length-pcmNs, dur=Math.round(size/this.bufferSampleRate*1000);
|
||
pcm0=pcm0.subarray(size);
|
||
pcms[0]=pcm0;
|
||
CLog($T("L8sC::延迟过大,已丢弃{1}ms {2}",0,dur,size),3);
|
||
}
|
||
return pcm0;
|
||
}
|
||
,_subPause:function(){//暂停了,就不要消费掉缓冲数据了,等待resume再来消费
|
||
var This=this;
|
||
if(!This.isPause){
|
||
return 0;
|
||
};
|
||
if(This.set.realtime){//实时模式,丢弃所有未消费的数据,resume时从最新input的数据开始播放
|
||
This._subClear();
|
||
};
|
||
return 1;
|
||
}
|
||
,_subClear:function(){ //清除缓冲数据
|
||
this.pcmBuffer=[[],[]];
|
||
}
|
||
,_subWrite:function(buffer, pcmSize, offset, speed){
|
||
var This=this;
|
||
var pcms=This.pcmBuffer;
|
||
var pcm0=pcms[0],pcm1=pcms[1];
|
||
|
||
//截取数据
|
||
var pcm=new Int16Array(pcmSize);
|
||
var i=0,n=0;
|
||
for(var j=0;n<pcmSize && j<pcm0.length;){//简单重采样
|
||
pcm[n++]=pcm0[i];
|
||
j+=speed; i=Math.round(j);
|
||
}
|
||
if(i>=pcm0.length){//堆积的消耗完了
|
||
pcm0=new Int16Array(0);
|
||
|
||
for(j=0,i=0;n<pcmSize && j<pcm1.length;){
|
||
pcm[n++]=pcm1[i];
|
||
j+=speed; i=Math.round(j);
|
||
}
|
||
if(i>=pcm1.length){
|
||
pcm1=new Int16Array(0);
|
||
}else{
|
||
pcm1=pcm1.subarray(i);
|
||
}
|
||
pcms[1]=pcm1;
|
||
}else{
|
||
pcm0=pcm0.subarray(i);
|
||
}
|
||
pcms[0]=pcm0;
|
||
|
||
|
||
//写入到audioBuffer中
|
||
var channel=buffer.getChannelData(0);
|
||
for(var i=0;i<pcmSize;i++,offset++){
|
||
channel[offset]=pcm[i]/0x7FFF;
|
||
}
|
||
|
||
This._Tc+=pcmSize;//更新已播放时长
|
||
This._updateTime();
|
||
|
||
return offset;
|
||
}
|
||
|
||
};
|
||
|
||
var NoStartMsg=function(){
|
||
return $T("TZPq::{1}未调用start方法",0,BufferStreamPlayerTxt);
|
||
};
|
||
|
||
|
||
|
||
/**pcm数据进行首尾1ms淡入淡出处理,播放时可以大幅减弱爆音**/
|
||
var FadeInOut=BufferStreamPlayer.FadeInOut=function(arr,sampleRate){
|
||
var sd=sampleRate/1000*1;//浮点数,arr是Int16或者Float32
|
||
for(var i=0;i<sd;i++){
|
||
arr[i]*=i/sd;
|
||
}
|
||
for(var l=arr.length,i=~~(l-sd);i<l;i++){
|
||
arr[i]*=(l-i)/sd;
|
||
}
|
||
};
|
||
|
||
/**解码音频文件成pcm**/
|
||
var DecodeAudio=BufferStreamPlayer.DecodeAudio=function(arrayBuffer,True,False){
|
||
var ctx=Recorder.GetContext();
|
||
if(!ctx){//强制激活Recorder.Ctx 不支持大概率也不支持解码
|
||
False&&False($T("iCFC::浏览器不支持音频解码"));
|
||
return;
|
||
};
|
||
if(!arrayBuffer || !(arrayBuffer instanceof ArrayBuffer)){
|
||
False&&False($T("wE2k::音频解码数据必须是ArrayBuffer"));
|
||
return;//非ArrayBuffer 有日志但不抛异常 不会走回调
|
||
};
|
||
|
||
ctx.decodeAudioData(arrayBuffer,function(raw){
|
||
var src=raw.getChannelData(0);
|
||
var sampleRate=raw.sampleRate;
|
||
|
||
var pcm=new Int16Array(src.length);
|
||
for(var i=0;i<src.length;i++){//floatTo16BitPCM
|
||
var s=Math.max(-1,Math.min(1,src[i]));
|
||
s=s<0?s*0x8000:s*0x7FFF;
|
||
pcm[i]=s;
|
||
};
|
||
|
||
True&&True({
|
||
sampleRate:sampleRate
|
||
,duration:Math.round(src.length/sampleRate*1000)
|
||
,data:pcm
|
||
});
|
||
},function(e){
|
||
False&&False($T("mOaT::音频解码失败:{1}",0,e&&e.message||"-"));
|
||
});
|
||
};
|
||
|
||
var CLog=function(){
|
||
var v=arguments; v[0]="["+BufferStreamPlayerTxt+"]"+v[0];
|
||
Recorder.CLog.apply(null,v);
|
||
};
|
||
Recorder[BufferStreamPlayerTxt]=BufferStreamPlayer;
|
||
|
||
|
||
})); |