From cbd272bc725923c915c825983277f8766bdbc0a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AB=98=E5=9D=9A=E6=9E=9C?= <753610399@qq.com> Date: Sat, 26 Oct 2019 15:00:13 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E8=AE=BE=E5=A4=87=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF=E6=97=B6=E6=8E=A5=E6=94=B6=E5=88=B0PCM=E7=BC=BA?= =?UTF-8?q?=E5=A4=B1=E5=AF=BC=E8=87=B4=E7=9A=84=E9=9F=B3=E9=A2=91=E5=8F=98?= =?UTF-8?q?=E7=9F=AD=20#51?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 ++- app-support-sample/README.md | 5 ++ dist/app-support/app.js | 2 +- dist/recorder-core.js | 2 +- index.html | 14 +++++- recorder.mp3.min.js | 2 +- recorder.wav.min.js | 2 +- src/app-support/app-native-support.js | 18 +++++-- src/app-support/app.js | 23 +++++---- src/recorder-core.js | 67 +++++++++++++++++++++++++-- 10 files changed, 118 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index b4f7b84..8d070d8 100644 --- a/README.md +++ b/README.md @@ -220,6 +220,8 @@ IOS其他浏览器|| *2019-07-22* 对[#34](https://github.com/xiangyuecn/Recorder/issues/34)反馈研究后发现,问题一:macOS、IOS的Safari对连续调用录音(中途未调用close)是有问题的,但只要调用close后再重复录音就没有问题。问题二:IOS上如果录音之前先播放了任何Audio,录音过程可能会变得很诡异,但如果先录音,就不存在此问题(19-09-18 Evan:QQ1346751357反馈发现本问题并非必现,[功能页面](https://hft.bigdatahefei.com/LocateSearchService/sfc/index),但本库的Demo内却必现,原因不明)。chrome、firefox正常的很。目测这两个问题是非我等屌丝能够解决的,于是报告给苹果家程序员看看,因此发了个[帖子](https://forums.developer.apple.com/message/373108),顺手在`Feedback Assistant`提交了`bug report`,但好几天过去了没有任何回应(顺带给微软一个好评)。 +*2019-10-26* 针对[#51](https://github.com/xiangyuecn/Recorder/issues/51)的问题研究后发现,如果录音时设备偶尔出现很卡的情况下(CPU被其他程序大量占用),浏览器采集到的音频是断断续续的,导致10秒的录音可能就只返回了5秒的数据量,这个时候最终编码得到的音频时长明显变短,播放时的效果就像快放一样。此问题能够稳定复现(使用别的程序大量占用CPU来模拟),目前已在`envIn`内部函数中进行了补偿处理,在浏览器两次传入PCM数据之间填充一段静默数据已弥补丢失的时长;最终编码得到的音频时长将和实际录音时长基本一致,消除了快放效果,但由于丢失的音频已被静默数据代替,听起来就是数据本身的断断续续的效果。在设备不卡时录音没有此问题。 + @@ -246,7 +248,7 @@ set={ //注意,取值不能过低,2048开始不同浏览器可能回调速率跟不上造成音质问题(低端浏览器→说的就是腾讯X5) ,onProcess:NOOP //接收到录音数据时的回调函数:fn(buffers,powerLevel,bufferDuration,bufferSampleRate) - //buffers=[[Int16,...],...]:缓冲的PCM数据,为从开始录音到现在的所有pcm片段;powerLevel:当前缓冲的音量级别0-100,bufferDuration:已缓冲时长,bufferSampleRate:缓冲使用的采样率(当type支持边录边转码(Worker)时,此采样率和设置的采样率相同,否则不一定相同) + //buffers=[[Int16,...],...]:缓冲的PCM数据,为从开始录音到现在的所有pcm片段,每次回调可能增加0-n个不定量的pcm片段;powerLevel:当前缓冲的音量级别0-100,bufferDuration:已缓冲时长,bufferSampleRate:缓冲使用的采样率(当type支持边录边转码(Worker)时,此采样率和设置的采样率相同,否则不一定相同) //如果需要绘制波形之类功能,需要实现此方法即可,使用以计算好的powerLevel可以实现音量大小的直观展示,使用buffers可以达到更高级效果 //注意,buffers数据的采样率和set.sampleRate不一定相同,可能为浏览器提供的原始采样率rec.srcSampleRate,也可能为已转换好的采样率set.sampleRate;如需浏览器原始采样率的数据,请使用rec.buffers数据,而不是本回调的参数;如需明确和set.sampleRate完全相同采样率的数据,请在onProcess中自行连续调用采样率转换函数Recorder.SampleData(),配合mock方法可实现实时转码和压缩语音传输 } @@ -302,7 +304,7 @@ set={ buffers中的PCM数据为浏览器采集的原始音频数据,采样率为浏览器提供的原始采样率`rec.srcSampleRate`;在`rec.set.onProcess`回调中`buffers`参数就是此数据或者此数据重新采样后的新数据。 -如果你需要长时间实时录音(如长时间语音通话),并且不需要得到最终完整编码的音频文件,Recorder初始化时应当使用一个未知的类型进行初始化(如: type:"unknown",仅仅用于初始化而已,实时转码可以手动转成有效格式,因为有效格式可能内部还有其他类型的缓冲),并且实时在`onProcess`中修改`rec.buffers`数组,只保留最后两个元素,其他元素设为null(代码:`rec.buffers[rec.buffers.length-3]=null`),以释放占用的内存,并且录音结束时不要调用`stop`(因为已录音的时间非常长时,stop操作会导致占用大量的内存,甚至不足),直接调用`close`丢弃所有数据即可。 +如果你需要长时间实时录音(如长时间语音通话),并且不需要得到最终完整编码的音频文件,Recorder初始化时应当使用一个未知的类型进行初始化(如: type:"unknown",仅仅用于初始化而已,实时转码可以手动转成有效格式,因为有效格式可能内部还有其他类型的缓冲),并且实时在`onProcess`中修改`rec.buffers`数组,只保留最后两个元素,其他元素设为null(代码:`rec.buffers[rec.buffers.length-3]=null`),以释放占用的内存,并且录音结束时可以不用调用`stop`,直接调用`close`丢弃所有数据即可。只要buffers[0]==null时调用`stop`永远会直接走fail回调。 ### 【属性】rec.srcSampleRate 浏览器提供的原始采样率,只有start或mock调用后才会有值,此采样率就是rec.buffers数据的采样率。 diff --git a/app-support-sample/README.md b/app-support-sample/README.md index 9bc0854..a9f0e9f 100644 --- a/app-support-sample/README.md +++ b/app-support-sample/README.md @@ -236,6 +236,11 @@ IOS-Weixin底层会把从微信素材下载过来的原始音频信息存储在s ## 【静态方法】RecordApp.Current.CanProcess() 识别的底层平台是否支持实时返回PCM数据,如果返回值为true,`set.onProcess`将可以被实时回调。 +## 【静态方法】RecordApp.GetStartUsedRecOrNull() +获取底层平台录音过程中会使用用来处理实时数据的Recorder对象实例rec,如果底层录音过程中不实用Recorder进行数据的实时处理,将返回null。除了微信平台外,其他平台均会返回rec,但Start调用前和Stop调用后均会返回null,只有Start后和Stop彻底完成前之间才会返回rec。 + +rec中的方法不一定都能使用,主要用来获取内部缓冲用的,比如:实时清理缓冲,当缓冲被清理,Stop时永远会走fail回调。 + ## 【静态属性】RecordApp.Platforms 支持的平台列表,目前有三个: 1. `Native`: 原生App平台支持,底层由实际的`JsBridge`提供,此平台默认未开启 diff --git a/dist/app-support/app.js b/dist/app-support/app.js index 3b75bca..35d4221 100644 --- a/dist/app-support/app.js +++ b/dist/app-support/app.js @@ -3,4 +3,4 @@ https://github.com/xiangyuecn/Recorder src: app-support/app.js,app-support/app-ios-weixin-support.js,app-support/app-native-support.js */ -!function(){"use strict";var n=/MicroMessenger/i.test(navigator.userAgent),e=window.RecordAppBaseFolder||"/Recorder/dist/",t=window.OnRecordAppInstalled,u=[{Key:"Native",Support:function(e){l.AlwaysAppUseJS?e(!1):o.Config.IsApp(e)},CanProcess:function(){return!0},Config:{IsApp:function(e){e(!1)},JsBridgeRequestPermission:function(e,n){n("JsBridgeRequestPermission未实现")},JsBridgeStart:function(e,n,t){t("JsBridgeStart未实现")},JsBridgeStop:function(e,n){n("JsBridgeStop未实现")},paths:[{url:e+"app-support/app-native-support.js",check:function(){return!o.IsInit}}]},ExtendDefault:!0},{Key:"IOS-Weixin",Support:function(e){l.AlwaysUseWeixinJS||!Recorder.Support()?e(n):e(!1)},CanProcess:function(){return!1},Config:{WxReady:function(e){e(null,"未实现IOS-Weixin.Config.WxReady")},DownWxMedia:function(e,n,t){t("下载素材接口DownWxMedia未实现")},paths:[{url:e+"app-support/app-ios-weixin-support.js",check:function(){return!r.IsInit}},{url:e+"engine/beta-amr.js",check:function(){return!Recorder.prototype.amr}}]},ExtendDefault:!0},{Key:"Default",Support:function(e){e(!0)},CanProcess:function(){return!0},Config:{paths:[{url:e+"recorder-core.js",check:function(){return!window.Recorder}},{url:e+"engine/mp3.js",check:function(){return!Recorder.prototype.mp3}}]}}],o=u[0],r=u[1],f=u[2];f.RequestPermission=function(e,n){var t=l.__Rec;t&&(t.close(),l.__Rec=null),Recorder().open(function(){e()},n)},f.Start=function(e,n,t){var o=l.__Rec;null!=o&&o.close(),l.__Rec=o=Recorder({type:e.type||"mp3",sampleRate:e.sampleRate||16e3,bitRate:e.bitRate||16,onProcess:e.onProcess||function(){}}),o.appSet=e,o.open(function(){o.start(),n()},function(e){t(e)})},f.Stop=function(t,n){var o=l.__Rec;if(!o)return Recorder.IsOpen()&&((o=Recorder()).open(),o.close()),void n("未开始录音");var r=function(){for(var e in o.close(),o.set)o.appSet[e]=o.set[e];l.__Rec=null};o.stop(function(e,n){r(),t(e,n)},function(e){r(),n(e)})};var l={LM:"2019-9-6 23:22:12",Current:0,IsWx:n,BaseFolder:e,AlwaysUseWeixinJS:!1,AlwaysAppUseJS:!1,Platforms:{Native:o,Weixin:r,Default:f},Js:function(r,i,s,e){var c=(e=e||window).document,a=function(e){if(e>=r.length)i();else{var n=r[e],t=n.url;if(!1!==n.check()){var o=c.createElement("script");o.setAttribute("type","text/javascript"),o.setAttribute("src",t),o.onload=function(){a(e+1)},o.onerror=function(e){s("请求失败:"+(e.message||"-")+","+t)},c.body.appendChild(o)}else a(e+1)}};a(0)},Install:function(t,o){var r=l.__reqs||(l.__reqs=[]);r.push({s:t,f:o}),t=function(){i("s",arguments)},o=function(e,n){i("f",arguments)};var i=function(e,n){for(var t=0;t=r.length)return v.decodeTime=Date.now()-u,void function(){c||(c=Date.now());for(var e=[],n=0;n=f.length)r();else{var e=f[t];g.DownWxMedia({mediaId:e,transform_mediaIds:f.join(","),transform_type:d.type,transform_bitRate:d.bitRate,transform_sampleRate:d.sampleRate},function(e){v.list.push(e),e.duration?r():/amr/i.test(e.mime)?(t++,i()):p("微信服务器返回了未知音频类型:"+e.mime)},function(e){p("下载音频失败:"+e)})}},s=0,c=function(){if(s>=n.length)return v.uploadTime=Date.now()-a,void i();var e=n[s];console.log("微信录音片段"+s+" wx.playVoice({localId:'"+e+"'})"),wx.uploadVoice({localId:e,isShowProgressTips:0,fail:p,success:function(e){var n=e.serverId;console.log("serverId:"+n),f.push(n),s++,c()}})},a=Date.now();c()},m=a.timeout;if(a.err)return console.error(a.err,m),void p("录制失败,已录制"+m.length+"分钟,但后面出错:"+a.err);if(m.length&&Date.now()-m[m.length-1].time<900)return R(),void t();c=0,a.wx.stopRecord({fail:p,success:function(e){var n=Date.now();m.push({res:e,duration:n-a.startTime,time:n}),t()}})}else p("未开始录音")}}(),function(){"use strict";var e=RecordApp.Platforms.Native,i=e.Config;e.IsInit=!0;var f=window.NativeRecordReceivePCM=window.top.NativeRecordReceivePCM=function(e,n){var t=f.rec;if(t){t._appStart||t.envStart(1,n),t._appStart=1;for(var o,r=atob(e),i=r.length,s=new Int16Array(i/2),c=0,a=0,u=0;u+2<=i;a++,u+=2)o=(r.charCodeAt(u)|r.charCodeAt(u+1)<<8)<<16>>16,s[a]=o,c+=Math.abs(o);t.envIn(s,c)}else console.error("未开始录音,但收到Native PCM数据")};e.RequestPermission=function(e,n){i.JsBridgeRequestPermission(e,n)},e.Start=function(e,n,t){f.param=e,f.rec=Recorder(e),i.JsBridgeStart(e,n,t)},e.Stop=function(o,r){i.JsBridgeStop(function(){var n=f.rec;if(f.rec=null,n){console.log("rec encode start: pcm:"+n.recSize+" src:"+n.srcSampleRate+" set:"+JSON.stringify(f.param));var t=function(){for(var e in n.set)f.param[e]=n.set[e]};n.stop(function(e,n){console.log("rec encode end"),t(),o(e,n)},function(e){t(),r(e)})}else r("未开始录音")},r)}}(); \ No newline at end of file +!function(){"use strict";var n=/MicroMessenger/i.test(navigator.userAgent),e=window.RecordAppBaseFolder||"/Recorder/dist/",t=window.OnRecordAppInstalled,u=[{Key:"Native",Support:function(e){l.AlwaysAppUseJS?e(!1):o.Config.IsApp(e)},CanProcess:function(){return!0},Config:{IsApp:function(e){e(!1)},JsBridgeRequestPermission:function(e,n){n("JsBridgeRequestPermission未实现")},JsBridgeStart:function(e,n,t){t("JsBridgeStart未实现")},JsBridgeStop:function(e,n){n("JsBridgeStop未实现")},paths:[{url:e+"app-support/app-native-support.js",check:function(){return!o.IsInit}}]},ExtendDefault:!0},{Key:"IOS-Weixin",Support:function(e){l.AlwaysUseWeixinJS||!Recorder.Support()?e(n):e(!1)},CanProcess:function(){return!1},Config:{WxReady:function(e){e(null,"未实现IOS-Weixin.Config.WxReady")},DownWxMedia:function(e,n,t){t("下载素材接口DownWxMedia未实现")},paths:[{url:e+"app-support/app-ios-weixin-support.js",check:function(){return!r.IsInit}},{url:e+"engine/beta-amr.js",check:function(){return!Recorder.prototype.amr}}]},ExtendDefault:!0},{Key:"Default",Support:function(e){e(!0)},CanProcess:function(){return!0},Config:{paths:[{url:e+"recorder-core.js",check:function(){return!window.Recorder}},{url:e+"engine/mp3.js",check:function(){return!Recorder.prototype.mp3}}]}}],o=u[0],r=u[1],f=u[2];f.RequestPermission=function(e,n){var t=l.__Rec;t&&(t.close(),l.__Rec=null),Recorder().open(function(){e()},n)},f.Start=function(e,n,t){var o=l.__Rec;null!=o&&o.close(),l.__Rec=o=Recorder(e),o.appSet=e,o.open(function(){o.start(),n()},function(e){t(e)})},f.Stop=function(t,n){var o=l.__Rec;if(!o)return Recorder.IsOpen()&&((o=Recorder()).open(),o.close()),void n("未开始录音");var r=function(){for(var e in o.close(),o.set)o.appSet[e]=o.set[e]};o.stop(function(e,n){r(),t(e,n),l.__Rec=null},function(e){r(),n(e),l.__Rec=null})};var l={LM:"2019-10-26 11:23:48",Current:0,IsWx:n,BaseFolder:e,AlwaysUseWeixinJS:!1,AlwaysAppUseJS:!1,Platforms:{Native:o,Weixin:r,Default:f},Js:function(r,i,s,e){var c=(e=e||window).document,a=function(e){if(e>=r.length)i();else{var n=r[e],t=n.url;if(!1!==n.check()){var o=c.createElement("script");o.setAttribute("type","text/javascript"),o.setAttribute("src",t),o.onload=function(){a(e+1)},o.onerror=function(e){s("请求失败:"+(e.message||"-")+","+t)},c.body.appendChild(o)}else a(e+1)}};a(0)},Install:function(t,o){var r=l.__reqs||(l.__reqs=[]);r.push({s:t,f:o}),t=function(){i("s",arguments)},o=function(e,n){i("f",arguments)};var i=function(e,n){for(var t=0;t=r.length)return v.decodeTime=Date.now()-u,void function(){c||(c=Date.now());for(var e=[],n=0;n=f.length)r();else{var e=f[t];m.DownWxMedia({mediaId:e,transform_mediaIds:f.join(","),transform_type:d.type,transform_bitRate:d.bitRate,transform_sampleRate:d.sampleRate},function(e){v.list.push(e),e.duration?r():/amr/i.test(e.mime)?(t++,i()):p("微信服务器返回了未知音频类型:"+e.mime)},function(e){p("下载音频失败:"+e)})}},s=0,c=function(){if(s>=n.length)return v.uploadTime=Date.now()-a,void i();var e=n[s];console.log("微信录音片段"+s+" wx.playVoice({localId:'"+e+"'})"),wx.uploadVoice({localId:e,isShowProgressTips:0,fail:p,success:function(e){var n=e.serverId;console.log("serverId:"+n),f.push(n),s++,c()}})},a=Date.now();c()},R=a.timeout;if(a.err)return console.error(a.err,R),void p("录制失败,已录制"+R.length+"分钟,但后面出错:"+a.err);if(R.length&&Date.now()-R[R.length-1].time<900)return g(),void t();c=0,a.wx.stopRecord({fail:p,success:function(e){var n=Date.now();R.push({res:e,duration:n-a.startTime,time:n}),t()}})}else p("未开始录音")}}(),function(){"use strict";var i=RecordApp,e=i.Platforms.Native,s=e.Config;e.IsInit=!0;var f=window.NativeRecordReceivePCM=window.top.NativeRecordReceivePCM=function(e,n){var t=f.rec;if(t){t._appStart||t.envStart(1,n),t._appStart=1;for(var o,r=atob(e),i=r.length,s=new Int16Array(i/2),c=0,a=0,u=0;u+2<=i;a++,u+=2)o=(r.charCodeAt(u)|r.charCodeAt(u+1)<<8)<<16>>16,s[a]=o,c+=Math.abs(o);t.envIn(s,c)}else console.error("未开始录音,但收到Native PCM数据")};e.RequestPermission=function(e,n){s.JsBridgeRequestPermission(e,n)},e.Start=function(e,n,t){f.param=e;var o=Recorder(e);o.set.disableEnvInFix=!0,f.rec=o,i.__Rec=o,s.JsBridgeStart(e,n,t)},e.Stop=function(o,n){var r=function(e){n(e),f.rec=null,i.__Rec=null};s.JsBridgeStop(function(){var n=f.rec;if(f.rec=null,n){console.log("rec encode start: pcm:"+n.recSize+" src:"+n.srcSampleRate+" set:"+JSON.stringify(f.param));var t=function(){for(var e in n.set)f.param[e]=n.set[e]};n.stop(function(e,n){console.log("rec encode end"),t(),o(e,n),i.__Rec=null},function(e){t(),r(e)})}else r("未开始录音")},r)}}(); \ No newline at end of file diff --git a/dist/recorder-core.js b/dist/recorder-core.js index 3529c33..8e48078 100644 --- a/dist/recorder-core.js +++ b/dist/recorder-core.js @@ -3,4 +3,4 @@ https://github.com/xiangyuecn/Recorder src: recorder-core.js */ -!function(l){"use strict";h.LM="2019-9-9 21:09:34";var m=function(){};function h(e){return new t(e)}function t(e){var t={type:"mp3",bitRate:16,sampleRate:16e3,bufferSize:4096,onProcess:m};for(var a in e)t[a]=e[a];this.set=t,this._S=9}h.IsOpen=function(){var e=h.Stream;if(e){var t=e.getTracks();if(0"+m.length+" 花:"+(Date.now()-n)+"ms"),setTimeout(function(){n=Date.now(),o[r.type](m,function(e){c(e,u)},function(e){i(e)})})}else i("未加载"+r.type+"编码器");else i("未采集到录音")}},l.Recorder=h}(window); \ No newline at end of file +!function(l){"use strict";C.LM="2019-10-26 11:23:58";var v=function(){};function C(e){return new t(e)}function t(e){var t={type:"mp3",bitRate:16,sampleRate:16e3,bufferSize:4096,onProcess:v};for(var n in e)t[n]=e[n];this.set=t,this._S=9}C.IsOpen=function(){var e=C.Stream;if(e){var t=e.getTracks();if(0"+v.length+" 花:"+(Date.now()-a)+"ms"),setTimeout(function(){a=Date.now(),r[s.type](v,function(e){c(e,p)},function(e){i(e)})})}else i("未加载"+s.type+"编码器");else i("音频被释放");else i("未采集到录音")}},l.Recorder=C}(window); \ No newline at end of file diff --git a/index.html b/index.html index a274b07..9c7aa89 100644 --- a/index.html +++ b/index.html @@ -60,7 +60,7 @@