-
Notifications
You must be signed in to change notification settings - Fork 1k
/
buffer_stream.player.js
860 lines (742 loc) · 31.9 KB
/
buffer_stream.player.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
/*
录音 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.gitee.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方法源码注释
//暂停播放,暂停后:实时模式下会丢弃所有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均有可能会导致顺序不一致
This.inputQueue[inputN]=pcm;
if(This._dest){//已start,可以开始处理队列
This._inputProcess();
}
}
,_inputErr:function(errMsg, inputN){
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++){
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();
}
}
/****************正常的播放处理****************/
//创建播放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];
if(pcm0.length+pcm1.length==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-pcm1.length);
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));
}
realMode=false;//切换成顺序播放
break;
}
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
pcm0=This._cutPcm0(3*sampleRate-wnSize);
speed=1.6;//倍速,重采样
//计算要截取出来量
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1.length)/speed));
break;
}
if(!realMode){
//*******按顺序取数据播放*********
//计算要截取出来量
pcmSize=Math.min(maxSize, pcm0.length+pcm1.length);
}
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];
var allSize=pcm0.length+pcm1.length;
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));
}
realMode=false;//切换成顺序播放
break;
}
//堆积的太多,要加速播放了,最多播放积压最后3秒的量,超过的直接丢弃
pcm0=This._cutPcm0(3*sampleRate);
speed=1.6;//倍速,重采样
//计算要截取出来量
pcmSize=Math.min(maxSize, Math.floor((pcm0.length+pcm1.length)/speed));
break;
}
if(!realMode){
//*******按顺序取数据播放*********
//计算要截取出来量
pcmSize=Math.min(maxSize, pcm0.length+pcm1.length);
}
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(pcm0.length>pcmNs){//丢弃超过秒数的
pcm0=pcm0.subarray(pcm0.length-pcmNs);
pcms[0]=pcm0;
}
return pcm0;
}
,_subPause:function(){//暂停了,就不要消费掉缓冲数据了,等待resume再来消费
var This=this;
if(!This.isPause){
return 0;
};
if(This.set.realtime){//实时模式,丢弃所有未消费的数据,resume时从最新input的数据开始播放
This.pcmBuffer=[[],[]];
};
return 1;
}
,_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;
}));