Слияние кода завершено, страница обновится автоматически
/*
录音 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;
}));
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )