Skip to content

Commit

Permalink
修复takeoffEncodeChunk在RecordApp Native端无效的bug,优化buffers内存清理逻辑
Browse files Browse the repository at this point in the history
  • Loading branch information
xiangyuecn committed May 17, 2020
1 parent 10930af commit cdfc24e
Show file tree
Hide file tree
Showing 11 changed files with 123 additions and 57 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ $.ajax({
1. [【Demo库】【格式转换】-mp3格式转成其他格式](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other)
2. [【Demo库】【格式转换】-wav格式转成其他格式](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.wav2other)
3. [【教程】实时转码并上传-通用版](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer)
4. [【教程】实时转码并上传-MP3专版](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer_mp3)
4. [【教程】实时转码并上传-mp3专版](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer_mp3)
5. [【Demo库】【文件合并】-mp3多个片段文件合并](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.merge.mp3_merge)
6. [【Demo库】【文件合并】-wav多个片段文件合并](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.merge.wav_merge)
7. [【教程】实时多路音频混音](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.mix_multiple)
Expand Down Expand Up @@ -310,7 +310,7 @@ IOS其他浏览器||

*2019-10-26* 针对[#51](https://github.com/xiangyuecn/Recorder/issues/51)的问题研究后发现,如果录音时设备偶尔出现很卡的情况下(CPU被其他程序大量占用),浏览器采集到的音频是断断续续的,导致10秒的录音可能就只返回了5秒的数据量,这个时候最终编码得到的音频时长明显变短,播放时的效果就像快放一样。此问题能够稳定复现(使用别的程序大量占用CPU来模拟),目前已在`envIn`内部函数中进行了补偿处理,在浏览器两次传入PCM数据之间填充一段静默数据已弥补丢失的时长;最终编码得到的音频时长将和实际录音时长基本一致,消除了快放效果,但由于丢失的音频已被静默数据代替,听起来就是数据本身的断断续续的效果。在设备不卡时录音没有此问题。

*2019-11-03* lamejs原版编码器编码出来的mp3文件首尾存在填充数据并且会占据一定时长(这种数据播放时静默,记录的信息数据或者填充),同一录音mp3格式的时长会比wav格式的时长要长0-100ms左右,大部分情况下不会有影响,但如果涉及到实时转码并传输的话,这些数据将会造成多段mp3片段的总时长比实际录音要长,最终播放时会均匀的感觉到停顿,并且mp3片段越小越明显。本库已对lamejs编码出来的mp3文件进行了处理,去掉了头部的非音频数据,但由于编码出来的mp3每一帧数据都有固定时长,文件结尾最后一帧可能录音的时长不能刚好填满,就会产生填充数据;因此本库编码出来的mp3文件会比wav格式长0-30ms左右,多出来的时长在mp3的结尾处;mp3解码出来的pcm数据直接去掉结尾多出来的部分,就和wav中的pcm数据基本一致了;另外可以通过调节待编码的pcm数据长度以达到刚好填满最后一帧来规避此问题,参考`Recorder.SampleData`方法提供的连续转码针对此问题的处理。[参考wiki](https://github.com/xiangyuecn/Recorder/wiki/lamejs编码出来的mp3时长修正)。
*2019-11-03* lamejs原版编码器编码出来的mp3文件首尾存在填充数据并且会占据一定时长(这种数据播放时静默,记录的信息数据或者填充),同一录音mp3格式的时长会比wav格式的时长要长0-100ms左右,大部分情况下不会有影响,但如果涉及到实时转码并传输的话,这些数据将会造成多段mp3片段的总时长比实际录音要长,最终播放时会均匀的感觉到停顿,并且mp3片段越小越明显。本库已对lamejs编码出来的mp3文件进行了处理,去掉了头部的非音频数据,但由于编码出来的mp3每一帧数据都有固定时长,文件结尾最后一帧可能录音的时长不能刚好填满,就会产生填充数据;因此本库编码出来的mp3文件会比wav格式长0-30ms左右,多出来的时长在mp3的结尾处;mp3解码出来的pcm数据直接去掉结尾多出来的部分,就和wav中的pcm数据基本一致了;另外可以通过调节待编码的pcm数据长度以达到刚好填满最后一帧来规避此问题,参考`Recorder.SampleData`方法提供的连续转码针对此问题的处理(但小的mp3片段拼接起来停顿导致的杂音还是非常明显,实时处理时使用`takeoffEncodeChunk`选项可完全避免此问题)。[参考wiki](https://github.com/xiangyuecn/Recorder/wiki/lamejs编码出来的mp3时长修正)。

*2020-04-26* Safari Bug:据QQ群内`1048506792``190451148`开发者反馈研究发现,IOS ?-13.X Safari内打开录音后,如果切换到了其他标签、或其他App并且播放了任何声音,此时将会中断已打开的录音(系统级的?),切换回正在录音的页面,这个页面的录音功能将会彻底失效,并且刷新也无法恢复录音;表现为关闭录音后再次打开录音,能够正常获得权限,但浏览器返回的采集到的音频为静默的PCM,此时地址栏也并未显示出麦克风图标,刷新这个标签也也是一样不能正常获得录音,只有关掉此标签新打开页面才可正常录音。如果打开录音后关闭了录音,然后切换到其他标签或App播放声音,然后返回录音页面,不会出现此问题。此为Safari的底层Bug(也许是少给临时工工钱了吧,无能为力)。使用长按录音类似的用户交互可大幅度避免踩到这坨翔。

Expand Down Expand Up @@ -552,7 +552,7 @@ wav格式编码器时参考网上资料写的,会发现代码和别人家的
### 简单将多段小的mp3片段合成长的mp3文件
由于lamejs CBR编码出来的mp3二进制数据从头到尾全部是大小相同的数据帧(采样率44100等无法被8整除的部分帧可能存在额外多1字节填充),没有其他任何多余信息,通过文件长度可计算出mp3的时长`fileSize*8/bitRate`[参考](https://blog.csdn.net/u010650845/article/details/53520426)),数据帧之间可以直接拼接。因此将小的mp3片段文件的二进制数据全部合并到一起即可得到长的mp3文件;要求待合成的所有mp3片段的采样率和比特率需一致。[mp3合并参考和测试+可移植源码](https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.merge.mp3_merge)

*注:CBR编码由于每帧数据的时长是固定的,mp3文件结尾最后这一帧的录音可能不能刚好填满,就会产生填充数据,多出来的这部分数据会导致mp3时长变长一点点,在实时转码传输时应当留意,解码成pcm后可直接去掉结尾的多余;另外可以通过调节待编码的pcm数据长度以达到刚好填满最后一帧来规避此问题,参考`Recorder.SampleData`方法提供的连续转码针对此问题的处理。首帧或前两帧可能是lame记录的信息帧,本库已去除,参考上面的已知问题。*
*注:CBR编码由于每帧数据的时长是固定的,mp3文件结尾最后这一帧的录音可能不能刚好填满,就会产生填充数据,多出来的这部分数据会导致mp3时长变长一点点,在实时转码传输时应当留意,解码成pcm后可直接去掉结尾的多余;另外可以通过调节待编码的pcm数据长度以达到刚好填满最后一帧来规避此问题,参考`Recorder.SampleData`方法提供的连续转码针对此问题的处理。首帧或前两帧可能是lame记录的信息帧,本库已去除(但小的mp3片段拼接起来停顿导致的杂音还是非常明显,实时处理时使用`takeoffEncodeChunk`选项可完全避免此问题),参考上面的已知问题。*


## beta-ogg (Vorbis)
Expand Down Expand Up @@ -705,6 +705,7 @@ set={
,spaceWidth:0 //柱子间空白固定基础宽度,柱子宽度自适应,当不为0时widthRatio无效,当视图不足容下所有柱子时将不会留空白,允许为负数,让柱子发生重叠
,minHeight:0 //柱子保留基础高度,position不为±1时应该保留点高度
,position:-1 //绘制位置,取值-1到1,-1为最底下,0为中间,1为最顶上,小数为百分比
,mirrorEnable:false //是否启用镜像,如果启用,视图宽度会分成左右两块,右边这块进行绘制,左边这块进行镜像(以中间这根柱子的中心进行镜像)

,stripeEnable:true //是否启用柱子顶上的峰值小横条,position不是-1时应当关闭,否则会很丑
,stripeHeight:3 //峰值小横条基础高度
Expand Down Expand Up @@ -830,7 +831,7 @@ public void onPermissionRequest(PermissionRequest request) {

编写本语音测试的目的在于验证H5录音实时转码、传输的可行性,并验证实时转码mp3格式小片段文件接收后的可播放性。经测试发现:除了移动端可能存在设备性能低下的问题以外,录音后实时转码mp3并传输给对方是可行的,对方接收后播放也能连贯的播放(效果还是要看播放代码写的怎么样,目前没有比较完美的播放代码)。另外(16kbps,16khz)MP3开语音15分钟大概3M的流量,wav 15分钟要37M多流量。

另外除wav外MP3等格式编码出来的音频的播放时间比PCM原始数据要长一些或短一些,如果涉及到解码或拼接时,这个地方需要注意。
另外除wav外MP3等格式编码出来的音频的播放时间比PCM原始数据要长一些或短一些,如果涉及到解码或拼接时,这个地方需要注意(如果类型支持,实时处理时使用`takeoffEncodeChunk`选项可完全避免此问题)

![](https://gitee.com/xiangyuecn/Recorder/raw/master/assets/use_webrtc.png)

Expand Down
8 changes: 4 additions & 4 deletions assets/npm-home/hash-history.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
[
{
"sha1": "8f3a1e18f39f4e401496b6911a40858bcf2a5c55",
"time": "2020-5-17 10:28:07"
},
{
"sha1": "0e8571bd082df5986d815921360eda7f64034ae5",
"time": "2020-5-16 23:29:15"
Expand All @@ -14,9 +18,5 @@
{
"sha1": "5eefc26c1cb43ba251693d4bf17ab4af3f681001",
"time": "2020-3-26 14:42:18"
},
{
"sha1": "bc0e623b290a1ad201c85d77a26d8081a2da22fc",
"time": "2020-3-3 20:57:11"
}
]
8 changes: 4 additions & 4 deletions assets/runtime-codes/teach.realtime.encode_transfer_mp3.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/******************
《【教程】实时转码并上传-MP3专版
《【教程】实时转码并上传-mp3专版
作者:高坚果
时间:2020-5-16 16:58:48
Expand All @@ -15,7 +15,7 @@ var testOutputWavLog=false;//顺带打一份wav的log,录音后执行mp3、wav
var testSampleRate=16000;
var testBitRate=16;

var SendInterval=300;//mp3 chunk数据会缓冲,当pcm的累积时长达到这个时长,就会传输发送
var SendInterval=300;//mp3 chunk数据会缓冲,当pcm的累积时长达到这个时长,就会传输发送。这个值在takeoffEncodeChunk实现下,使用0也不会有性能上的影响。

//重置环境
var RealTimeSendTryReset=function(){
Expand Down Expand Up @@ -103,13 +103,13 @@ var RealTimeOnProcessClear=function(buffers,powerLevel,bufferDuration,bufferSamp
realTimeSendTryWavTestBuffers=[];
realTimeSendTryWavTestSampleRate=0;
};
realTimeSendTryClearPrevBufferIdx=newBufferIdx;

//清理PCM缓冲数据,最后完成录音时不能调用stop,因为数据已经被清掉了
//这里进行了延迟操作,只清理上次到现在的buffer
//这里进行了延迟操作(必须要的操作),只清理上次到现在的buffer
for(var i=realTimeSendTryClearPrevBufferIdx;i<newBufferIdx;i++){
buffers[i]=null;
};
realTimeSendTryClearPrevBufferIdx=newBufferIdx;

//备份一下方便后面生成测试wav
for(var i=newBufferIdx;i<buffers.length;i++){
Expand Down
97 changes: 75 additions & 22 deletions assets/zdemo.index.webrtc.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Recorder.Support();//激活Recorder.Ctx

/*********注入界面******************/
(function(){
reclog("<span style='color:#f60'>实时编码除wav格式外发送间隔尽量不要低于编码速度速度,除wav外其他格式编码结果可能会比实际的PCM结果音频时长略长或略短,如果涉及到实时解码应留意此问题,长了的时候可截断首尾使解码后的PCM长度和录音的PCM长度一致(可能会增加噪音)</span>");
reclog("<span style='color:#f60'>实时编码未使用takeoffEncodeChunk实现时:除wav格式外发送间隔尽量不要低于编码速度速度,除wav外其他格式编码结果可能会比实际的PCM结果音频时长略长或略短,如果涉及到实时解码应留意此问题,长了的时候可截断首尾使解码后的PCM长度和录音的PCM长度一致(可能会增加噪音)</span>,<span style='color:#0b1'>使用takeoffEncodeChunk实现时无此限制</span>;参考<a target='_blank' href='https://xiangyuecn.github.io/Recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer_mp3'>【教程】实时转码并上传-mp3专版</a>。");

var i=0;
reclog(['已开启实时编码传输模拟'
Expand All @@ -20,7 +20,7 @@ Recorder.Support();//激活Recorder.Ctx
,(++i)+'. 复制设备B"本机信息"到设备A的"远程信息"中,设备A中点击确定连接'
,(++i)+'. 连接已建立,任何一方都可随时开始录音,并且数据都会发送给另外一方'
,''
,'关于传输到对方播放时音质变差(有噪音)的问题,没找到这种小片段接续播放的现成实现,播放代码都是反复测试出来的,最差也就这样子了。两个播放模式的音质wav算是勉强最好,MP3差点。(16kbps,16khz)MP3开语音15分钟大概3M的流量,wav 15分钟要37M多流量。另外MP3编码出来的音频的播放时间比PCM原始数据要长一些,这个地方需要注意。'
,'关于传输到对方播放时音质变差(有噪音)的问题,没找到这种小片段接续播放的现成实现,播放代码都是反复测试出来的,最差也就这样子了。两个播放模式的音质wav算是勉强最好,MP3差点。(16kbps,16khz)MP3开语音15分钟大概3M的流量,wav 15分钟要37M多流量。另外MP3编码出来的音频的播放时间比PCM原始数据要长一些,这个地方需要注意(本例子默认采用的是onProcess+mock实现,因此会有停顿杂音,如果开启takeoffEncodeChunk实现就无此限制,参考下面日志里的链接)。'
,''
,'<span style="color:red">本demo仅支持局域网</span>(无需服务器支持),采用WebRTC P2P传输数据,如果要支持公网访问会异常复杂,实际使用时用WebSocket来进行数据传输数据会简单很多'
,''
Expand Down Expand Up @@ -100,6 +100,7 @@ var realTimeSendTryEncBusy;
var realTimeSendTryChunk;
var realTimeSendTryChunks;
var realTimeSendTryChunkSampleRate;
var realTimeSendTryTakeoffChunksIdx;
var realTimeSendTryReset=function(){
realTimeSendTryTime=0;
};
Expand All @@ -115,6 +116,7 @@ var realTimeSendTry=function(recSet,interval,pcmDatas,sampleRate){
realTimeSendTryEncBusy=0;
realTimeSendTryChunk=null;
realTimeSendTryChunks=[];
realTimeSendTryTakeoffChunksIdx=0;

if(recSet.type=="wav"){
reclog("<span style='color:#0b1'>实时编码wav格式很快,无需任何优化</span>");
Expand All @@ -123,6 +125,13 @@ var realTimeSendTry=function(recSet,interval,pcmDatas,sampleRate){
}else{
reclog("<span style='color:#f60'>实时编码"+recSet.type+"格式采用UI线程转码,将会有明显卡顿</span>");
};

if(recSet.takeoffEncodeChunk){
reclog("已开启takeoffEncodeChunk实现,通过接管实时编码输出,避免了音频片段首尾的静默停顿导致的杂音",2);
if(recSet.type!="mp3"){
reclog("当前测试代码只提供了mp3格式的takeoffEncodeChunk处理,请升级代码",1);
};
};
};
if(t1-realTimeSendTryTime<interval){
return;
Expand All @@ -135,13 +144,56 @@ var realTimeSendTry=function(recSet,interval,pcmDatas,sampleRate){
realTimeSendTryChunks.push(chunk.data);
realTimeSendTryChunkSampleRate=chunk.sampleRate;

var recMock=Recorder($.extend({},recSet));
recMock.mock(chunk.data,chunk.sampleRate);

if(recImpl[recSet.type+"_start"]){
endT=Date.now();
};

var st1=Date.now();
var encodeEnd=function(tag,blob,duration){
//此处应伪装成发送blob数据
//emmmm.... 发送给语音聊天webrtc
if(window.rtcChannelOpen){
webrtcStreamSend(blob,{
duration:duration
,interval:interval
});
return;
};

addRecLog(duration,tag,blob,recSet,st1);
};

//已接管编码器输出
if(recSet.takeoffEncodeChunk && recSet.type=="mp3"){
//合并所有的音频片段
var len=0;
for(var i=realTimeSendTryTakeoffChunksIdx;i<takeoffChunks.length;i++){
len+=takeoffChunks[i].length;
};
var chunkData=new Uint8Array(len);
for(var i=realTimeSendTryTakeoffChunksIdx,idx=0;i<takeoffChunks.length;i++){
var itm=takeoffChunks[i];
chunkData.set(itm,idx);
idx+=itm.length;
};
realTimeSendTryTakeoffChunksIdx=takeoffChunks.length;

//生成完整blob音频文件
var blob=new Blob([chunkData],{type:"audio/"+recSet.type});
var meta=Recorder.mp3ReadMeta([chunkData.buffer],len)||{};

var tag=realTimeSendTryChunks.length+"接管音频输出";
encodeEnd(tag,blob,meta.duration);
return;
};

//mock转码
var mockSet=$.extend({},recSet);
mockSet.takeoffEncodeChunk=null;
var recMock=Recorder(mockSet);
recMock.mock(chunk.data,chunk.sampleRate);

if(realTimeSendTryEncBusy>=2){
if(window.rtcChannelOpen){
rtcSendSkip++;
Expand All @@ -153,23 +205,19 @@ var realTimeSendTry=function(recSet,interval,pcmDatas,sampleRate){
};
realTimeSendTryEncBusy++;

recstopFn(null,0,function(err,blob,duration){
recMock.stop(function(blob,duration){
realTimeSendTryEncBusy&&(realTimeSendTryEncBusy--);

//此处应伪装成发送blob数据
//emmmm.... 发送给语音聊天webrtc
if(blob&&window.rtcChannelOpen){
webrtcStreamSend(blob,{
duration:duration
,interval:interval
});
return -1;
};

var ms=(endT||Date.now())-t1;
var max=recImpl[recSet.type+"_start"]?99999:1000*pcmDatas[0].length/sampleRate-10;//录音回调1帧时长
return realTimeSendTryChunks.length+"实时编码占用<span style='color:"+(ms>max?"red":"")+"'>"+ms+"ms</span>";
},recMock);

var tag=realTimeSendTryChunks.length+"实时编码占用<span style='color:"+(ms>max?"red":"")+"'>"+ms+"ms</span>";
encodeEnd(tag,blob,duration);
},function(err){
realTimeSendTryEncBusy&&(realTimeSendTryEncBusy--);

reclog("实时编码失败:"+err,1);
});
};
var realTimeSendTryStop=function(recSet){
if(!realTimeSendTryTime){
Expand All @@ -179,11 +227,16 @@ var realTimeSendTryStop=function(recSet){
//借用SampleData函数把二维数据转成一维,采样率转换是次要的
var chunk=Recorder.SampleData(realTimeSendTryChunks,realTimeSendTryChunkSampleRate,realTimeSendTryChunkSampleRate);

var recMock=Recorder($.extend({},recSet));
var mockSet=$.extend({},recSet);
mockSet.takeoffEncodeChunk=null;
var recMock=Recorder(mockSet);
recMock.mock(chunk.data,chunk.sampleRate);
recstopFn(null,0,function(err,blob,time){
return "实时编码汇总结果";
},recMock);
var mockT=Date.now();
recMock.stop(function(blob,duration){
addRecLog(duration,"实时编码汇总结果",blob,mockSet,mockT);
},function(err){
reclog("实时编码汇总失败:"+err,1);
});
};


Expand Down Expand Up @@ -322,7 +375,7 @@ function webrtcVoiceSend(data){
function webrtcReceive(data){
var m=/^(.+?)##(.+?)##(?:txt:([\S\s]*)|data:(.+?);\s*base64\s*,\s*(.+))?$/.exec(data);
if(!m){
console.warn("webrtc收到未知数据:",data);
rtcMsgView("webrtc收到未知数据:"+data,true);
return;
};
var type=m[1];
Expand Down
Loading

0 comments on commit cdfc24e

Please sign in to comment.