This repository has been archived on 2025-05-21. You can view files and clone it, but cannot push or open issues or pull requests.
mp3-to-text/public/extensions/buffer_stream.player.js
2025-04-20 18:46:48 +08:00

887 lines
33 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
录音 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;
}));