+> npm install +> npm run build-dev ++ 然后就可以打开index.html查看效果了。
diff --git a/QuickStart.html b/QuickStart.html index 24d1686..2a13d84 100644 --- a/QuickStart.html +++ b/QuickStart.html @@ -85,9 +85,9 @@
@@ -115,15 +115,14 @@ -'+"at:"+lineNo+":"+columnNo+" url:"+url+"\n"+(error&&error.stack||"不能获得错误堆栈")+''); }; -reclog("RecordApp[即将废弃] 除Recorder支持的外,支持Hybrid App,低版本IOS上支持微信网页和小程序web-view"+unescape("%uD83C%uDF89"),"#f60;font-weight:bold;font-size:24px"); +reclog('如需录音功能定制开发,网站、App、小程序、前端后端开发等需求,请加QQ群:①群 781036591、②群 748359095,口令recorder,联系群主(即作者),谢谢~',"#f60;font-size:22px;font-weight:bold"); reclog("Recorder H5使用简单,功能丰富,支持PC、Android、IOS 14.3+"+unescape("%uD83D%uDCAA"),"#0b1;font-weight:bold;font-size:24px"); -reclog('本页面修改时间(有可能修改了忘改):2022-05-24 19:31:18',"#999"); +reclog('本页面修改时间(有可能修改了忘改):2022-08-07 18:20',"#999"); reclog('Recorder库修改时间(有可能修改了忘改):'+(window.Recorder&&Recorder.LM),"#999"); +reclog("UA: "+navigator.userAgent, "#999"); +reclog("URL: "+location.href.replace(/#.*/g,""), "#999"); reclog(Tips); +reclog('当前浏览器支持录音':'red">不支持录音')+''); if(window.useCDN && useCDN.cdn){ reclog('本页面的js资源采用的CDN不稳定,已切换到:'+useCDN.cdn,'#f60'); diff --git a/README.md b/README.md index d599d8c..a8daa60 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # :open_book:Recorder用于html5录音 -[](?Ref=Desc&Start)支持在大部分已实现`getUserMedia`的移动端、PC端浏览器麦克风录音、实时处理,主要包括:Chrome、Firefox、Safari、iOS 14.3+、Android WebView、腾讯Android X5内核(QQ、微信)、大部分2021年后更新的Android手机自带浏览器;不支持:~~UC系内核(典型的支付宝),大部分未更新的老旧国产手机自带浏览器,低版本iOS(11.0-14.2)上除Safari外的其他任何形式的浏览器(含PWA、WebClip、任何App内网页)~~。 +[](?Ref=Desc&Start)支持在大部分已实现`getUserMedia`的移动端、PC端浏览器麦克风录音、实时处理,主要包括:Chrome、Firefox、Safari、iOS 14.3+、Android WebView、腾讯Android X5内核(QQ、微信、小程序WebView)、大部分2021年后更新的Android手机自带浏览器;不支持:~~UC系内核(典型的支付宝),大部分未更新的老旧国产手机自带浏览器,低版本iOS(11.0-14.2)上除Safari外的其他任何形式的浏览器(含PWA、WebClip、任何App内网页)~~。 支持对任意`MediaStream`进行音频录制、实时处理,包括:`getUserMedia返回的流`、`WebRTC中的remote流`、`audio、video标签的captureStream方法返回的流`、`自己创建的流` 等等。 @@ -11,7 +11,7 @@ 音频文件的播放:可直接使用常规的`Audio HTML标签`来播放完整的音频文件,参考文档下面的【快速使用】部分,有播放例子;上传了的录音直接将音频链接赋值给`audio.src`即可播放;本地的`blob音频文件`可通过`URL.createObjectURL`来生成本地链接赋值给`audio.src`即可播放,或者将blob对象直接赋值给`audio.srcObject`(兼容性没有src高)。实时的音频片段文件播放,可以使用本库自带的`BufferStreamPlayer`插件来播放,简单高效,或者采用别的途径播放。 -**如需录音功能定制,网站、App、小程序开发等需求,请加本文档下面的QQ群,联系群主(即作者),谢谢~** +**如需录音功能定制开发,网站、App、小程序、前端后端开发等需求,请加本文档下面的QQ群,联系群主(即作者),谢谢~** [](?) @@ -49,7 +49,7 @@ > mp3默认16kbps的比特率,2kb每秒的录音大小,音质还可以(如果使用8kbps可达到1kb每秒,不过音质太渣)。主要用于语音录制,双声道语音没有意义,特意仅对单声道进行支持。mp3、wav、pcm格式支持边录边转码,录音结束时转码速度极快,支持实时转码成小片段文件和实时传输,demo中已实现一个语音通话聊天,下面有介绍;其他格式录音结束时可能需要花费比较长的时间进行转码。 > -> mp3使用lamejs编码(CBR),压缩后的recorder.mp3.min.js文件150kb左右(开启gzip后54kb)。如果对录音文件大小没有特别要求,可以仅仅使用录音核心+wav编码器(raw pcm format录音文件超大),压缩后的recorder.wav.min.js不足12kb。录音得到的mp3(CBR)、wav(PCM),均可简单拼接小的二进制录音片段文件来生成长的音频文件,具体参考下面这两种编码器的详细介绍。 +> mp3使用lamejs编码(CBR),压缩后的recorder.mp3.min.js文件160kb左右(开启gzip后60kb)。如果对录音文件大小没有特别要求,可以仅仅使用录音核心+wav编码器(raw pcm format录音文件超大),压缩后的recorder.wav.min.js不足20kb。录音得到的mp3(CBR)、wav(PCM),均可简单拼接小的二进制录音片段文件来生成长的音频文件,具体参考下面这两种编码器的详细介绍。 > 对于不支持录音的浏览器,引入js和调用相关方法都不会产生异常(IE8+),会进入相关的fail回调;一般在open的时候就能检测到不支持或被用户拒绝了权限,可在用户开始录音之前提示浏览器不支持录音或授权。 @@ -68,7 +68,7 @@ [](?) ### Demo片段列表 -1. [【Demo库】【格式转换】-mp3格式转成其他格式](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other) +1. [【Demo库】【格式转换】-mp3等格式解码转成其他格式](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other) 2. [【Demo库】【格式转换】-wav格式转成其他格式](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.wav2other) 3. [【Demo库】【格式转换】-amr格式转成其他格式](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.amr2other) 4. [【教程】【音频流】【上传】实时转码并上传-通用版](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer) @@ -84,7 +84,8 @@ 14. [【教程】新录音从老录音接续、或录制中途插入音频](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.env_in.follow) 15. [【教程】DTMF(电话拨号按键信号)解码、编码](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.dtmf.decode_and_encode) 16. [【Demo库】PCM采样率提升](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.samplerate.raise) -17. [【测试】音频可视化相关插件测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.extensions.visualization) +17. [【测试】WebM格式解析并提取音频](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.webm.extract_audio) +18. [【测试】音频可视化相关插件测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.extensions.visualization) ### App Demo @@ -100,7 +101,7 @@ iOS Demo App :[下载源码](https://github.com/xiangyuecn/Recorder/tree/maste ## 【QQ群】交流与支持 -欢迎加QQ群:781036591,纯小写口令:`recorder` +欢迎加QQ群:①群 781036591、②群 748359095,纯小写口令:`recorder` @@ -174,7 +175,7 @@ iOS Demo App :[下载源码](https://github.com/xiangyuecn/Recorder/tree/maste **方式二**:通过import/require引入 -通过 npm/cnpm 进行安装 `npm install recorder-core` ,如果直接clone的源码下面文件路径调整一下即可 [](?Ref=ImportCode&Start) +通过 npm 进行安装 `npm install recorder-core` ,如果直接clone的源码下面文件路径调整一下即可 [](?Ref=ImportCode&Start) ``` javascript //必须引入的核心,换成require也是一样的。注意:recorder-core会自动往window下挂载名称为Recorder对象,全局可调用window.Recorder,也许可自行调整相关源码清除全局污染 import Recorder from 'recorder-core' @@ -188,6 +189,8 @@ import 'recorder-core/src/engine/mp3-engine' //如果此格式有额外的编码 //可选的插件支持项 import 'recorder-core/src/extensions/waveview' + +//ts import 提示:npm包内已自带了.d.ts声明文件(不过是any类型) ``` [](?RefEnd) @@ -418,17 +421,16 @@ iOS 11.0-14.2:纯粹的H5录音在iOS WebView中是不支持的,需要有Nat 浏览器Audio Media[兼容性](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility)mp3最好,wav还行,其他要么不支持播放,要么不支持编码;因此本库最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式。 -**特别注**:低版本`iOS(11.X、12.X、13.X)`上只有`Safari`支持`getUserMedia`,低版本iOS上其他浏览器均不支持,唯一有点卵用的Safari `getUserMedia` 底层实现bug奇多,参考下面的已知问题。 -**特别注**:大部分2021年以前的老旧国产手机自带的浏览器(系统浏览器)虽然支持`getUserMedia`方法,但并不能使用,表现为直接返回拒绝或者干脆没有任何回调;UC系列目测全部阵亡(含支付宝)。 +**留意中途来电话**:在移动端录音时,如果录音中途来电话,或者通话过程中打开录音,是不一定能进行录音的;经过简单测试发现,IOS上Safari将暂停返回音频数据,直到通话结束才开始继续有音频数据返回;小米上Chrome不管是来电还是通话中开始录音都能对麦克风输入的声音进行录音;只是简单测试,更多机器和浏览器并未做测试,不过整体上来看来电话或通话中进行录音的可行性并不理想,也不赞成在这种过程中进行录音;但只要通话结束后录音还是会正常进行,影响基本不大。 -**留意中途来电话**:在移动端录音时,如果录音中途来电话,或者通话过程中打开录音,是不一定能进行录音的;经过简单测试发现,IOS上Safari将暂停返回音频数据,直到通话结束才开始继续有音频数据返回;小米上Chrome不管是来电还是通话中开始录音都能对麦克风输入的声音进行录音(听筒中的并不能录到,扬声器外放的会被明显降噪);只是简单测试,更多机器和浏览器并未做测试,不过整体上来看来电话或通话中进行录音的可行性并不理想,也不赞成在这种过程中进行录音;但只要通话结束后录音还是会正常进行,影响基本不大。 +**录音时对播放音频的影响**:仅在移动端,如果录音参数中启用了降噪+回声消除,打开录音后,如果同时播放音频,此时播放声音可能会变得很小;PC上 和 禁用降噪+回声消除后 似乎无此影响。 -**录音时对播放音频的影响**:仅在移动端,录音过程中尽量不要去播放音频,正在播放的也应该暂停播放,否则不同手机系统、浏览器环境可能表现会出乎意料;已知IOS Safari上录音打开后,如果播放音频,声音会[变得非常小](https://www.cnblogs.com/cocoajin/p/7591068.html);Android上也有可能声音被切换到听筒播放,而不是扬声器大喇叭上播放导致声音也会变小;更多可能的情况需要更多设备、浏览器的测试数据才能发掘;PC上似乎无此影响。 +**移动端锁屏录音**:手机锁屏后浏览器的运行状态是一个玄学,是否能录音不可控;不同手机、甚至同一手机在不同状态下,有可能能录又有可能不能录,且无法检测;可以调用 `navigator.wakeLock` 来阻止手机自动锁屏,不支持的直接简单粗暴的 循环+静音 播放一段视频,来阻止锁屏,就是有点费电,具体实现可参考H5在线测试页面内的`wakeLockClick`方法。 **特别注**:如果在`iframe`里面调用的录音功能,并且和上层的网页是不同的域(跨域了),如果未设置相应策略,权限永远是被拒绝的,[参考此处](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#Privacy_and_security)。另外如果要在`非跨域的iframe`里面使用,最佳实践应该是让window.top去加载Recorder(异步加载js),iframe里面使用top.Recorder,免得各种莫名其妙(比如微信里面的各种渣渣功能,搞多了就习惯了)。 -> 如果需要最大限度的兼容低版本IOS(仅增加微信支持),可以使用`RecordApp`,它已包含`Recorder`,源码在`src/app-support`、`app-support-sample`中,但此兼容库需要服务器端提供微信JsSDK的签名、下载素材接口,涉及微信公众(订阅)号的开发。 +> 低版本`iOS(11.X、12.X、13.X)`上只有`Safari`支持`getUserMedia`,其他任何形式的浏览器均不支持;如果需要最大限度的兼容低版本IOS(仅增加微信支持),可以使用`RecordApp`,它已包含`Recorder`,源码在`src/app-support`、`app-support-sample`中,但此兼容库需要服务器端提供微信JsSDK的签名、下载素材接口,涉及微信公众(订阅)号的开发。 支持|Recorder|~[RecordApp](https://github.com/xiangyuecn/Recorder/tree/master/app-support-sample)~ -:|:-:|:-: @@ -449,21 +451,10 @@ iOS其他浏览器|iOS 14.3+|iOS 14.3+ ## 已知问题 -*2018-09-19* [caniuse](https://caniuse.com/#search=getUserMedia) 注明`IOS` `11.X - 12.X(13.X)` 上 只有`Safari`支持调用`getUserMedia`,其他App下WKWebView(UIWebView?)([相关资料](https://forums.developer.apple.com/thread/88052))均不支持(2021-2-15 IOS 14.3+已无此问题)。经用户测试验证IOS 12上chrome、UC都无法录音,部分IOS 12 Safari可以获取到并且能正常录音,但部分不行,原因未知,参考[ios 12 支不支持录音了](https://www.v2ex.com/t/490695)。在IOS上不支持录音的环境下应该采用其他解决方案,参考`案例演示`、`关于微信JsSDK`部分。 - -*2019-02-28* [issues#14](https://github.com/xiangyuecn/Recorder/issues/14) 如果`getUserMedia`返回的[`MediaStreamTrack.readyState == "ended"`,`"ended" which indicates that the input is not giving any more data and will never provide new data.`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack) ,导致无法录音。如果产生这种情况,目前在`rec.open`方法调用时会正确检测到,并执行`fail`回调。造成`issues#14` `ended`原因是App源码中`AndroidManifest.xml`中没有声明`android.permission.MODIFY_AUDIO_SETTINGS`权限,导致腾讯X5不能正常录音。 - -*2019-03-09* 在Android上QQ、微信里,请求授权使用麦克风的提示,经过长时间观察发现,他们的表现很随机、很奇特。可能每次在调用`getUserMedia`时候都会弹选择,也可能选择一次就不会再弹提示,也可能重启App后又会弹。如果用户拒绝了,可能第二天又会弹,或者永远都不弹了,要么重置(装)App。使用腾讯X5内核的App测试也是一样奇特表现,拒绝权限后可能必须要重置(装)。这个问题貌似跟X5内核自动升级的版本有关。QQ浏览器更加惨不忍睹,2019-08-16测试发现卸载重装、拒绝权限后永远无法弹出授权,通过浏览器设置-清理-清理地理位置授权才能恢复,重启、重装、清理系统垃圾、删除根目录文件夹(腾讯那个大文件不敢删,毒瘤)垃圾均无效,奇葩。 - -*2019-06-14* 经[#29](https://github.com/xiangyuecn/Recorder/issues/29)反馈,稍微远程真机测试了部分厂商的比较新的Android手机系统浏览器的录音支持情况;华为:直接返回拒绝,小米:没有回调,OPPO:好像是没有回调,vivo:好像是没有回调;另外专门测试了一下UC最新版(支付宝):直接返回拒绝。另[参考](https://www.jianshu.com/p/6cd5a7fa562c)。也许他们都商量好了或者本身都是用的UC?至于没有任何回调的,此种浏览器没有良心。 - -*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`,但好几天过去了没有任何回应(顺带给微软一个好评)。问题一目前已通过全局共享一个MediaStream连接来解决,原因在于Safari上MediaStream断开后就无法再次进行连接使用(表现为静音),改成了全局只连接一次就避免了此问题;全局处理也有利于屏蔽底层细节,start时无需再调用底层接口,提升兼容、可靠性。 - -*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`方法提供的连续转码针对此问题的处理(但小的mp3片段拼接起来停顿导致的杂音还是非常明显,实时处理时使用`takeoffEncodeChunk`选项可完全避免此问题)。[参考wiki](https://github.com/xiangyuecn/Recorder/wiki/lamejs编码出来的mp3时长修正)。 +> 此处已清除7个已知问题,大部分无法解决的问题会随着时间消失;问题主要集中在iOS上,好在这玩意能更新 -*2020-04-26* Safari Bug:据QQ群内`1048506792`、`190451148`开发者反馈研究发现,IOS ?-13.X Safari内打开录音后,如果切换到了其他标签、或其他App并且播放了任何声音,此时将会中断已打开的录音(系统级的?),切换回正在录音的页面,这个页面的录音功能将会彻底失效,并且刷新也无法恢复录音;表现为关闭录音后再次打开录音,能够正常获得权限,但浏览器返回的采集到的音频为静默的PCM,此时地址栏也并未显示出麦克风图标,刷新这个标签也也是一样不能正常获得录音,只有关掉此标签新打开页面才可正常录音。如果打开录音后关闭了录音,然后切换到其他标签或App播放声音,然后返回录音页面,不会出现此问题。此为Safari的底层Bug(也许是少给临时工工钱了吧,无能为力)。使用长按录音类似的用户交互可大幅度避免踩到这坨翔。 +*2020-04-26* Safari Bug:据QQ群内`1048506792`、`190451148`开发者反馈研究发现,IOS ?-13.X Safari内打开录音后,如果切换到了其他标签、或其他App并且播放了任何声音,此时将会中断已打开的录音(系统级的?),切换回正在录音的页面,这个页面的录音功能将会彻底失效,并且刷新也无法恢复录音;表现为关闭录音后再次打开录音,能够正常获得权限,但浏览器返回的采集到的音频为静默的PCM,此时地址栏也并未显示出麦克风图标,刷新这个标签也也是一样不能正常获得录音,只有关掉此标签新打开页面才可正常录音。如果打开录音后关闭了录音,然后切换到其他标签或App播放声音,然后返回录音页面,不会出现此问题。此为Safari的底层Bug。使用长按录音类似的用户交互可大幅度避免踩到这坨翔。 @@ -519,7 +510,8 @@ set={ //注意:流内必须至少存在一条音轨(Audio Track),比如audio标签必须等待到可以开始播放后才会有音轨,否则open会失败 //,audioTrackSet:{ deviceId:"",groupId:"", autoGainControl:true, echoCancellation:true, noiseSuppression:true } - //普通麦克风录音时getUserMedia方法的audio配置参数,比如指定设备id,回声消除、降噪开关;注意:提供的任何配置值都不一定会生效 + //普通麦克风录音时getUserMedia方法的audio配置参数;注意:提供的任何配置值都不一定会生效 + //回声消除、降噪开关这两个已默认明确关闭,开启可能会导致移动端表现的很怪异,包括系统播放声音变小,如需开启请测试好后再开启 //由于麦克风是全局共享的,所以新配置后需要close掉以前的再重新open //更多参考: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints @@ -540,17 +532,15 @@ set={ ### 【方法】rec.open(success,fail) 请求打开录音资源,如果浏览器不支持录音、用户拒绝麦克风权限、或者非安全环境(非https、file等)将会调用`fail`;打开后需要调用`close`来关闭,因为浏览器或设备的系统可能会显示正在录音。 -注意:此方法回调是可能是同步的(异常、或者已持有资源时)也可能是异步的(浏览器弹出权限请求时);一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音。 - -另外:普通的麦克风录音时,因为此方法会调起用户授权请求,如果仅仅想知道浏览器是否支持录音(比如:如果浏览器不支持就走另外一套录音方案),应使用`Recorder.Support()`方法。 +`success`=fn(); -**注意:打开普通麦克风录音后,如果未调用close关闭,可能会影响audio音频的播放,表现为移动端audio播放有明显的杂音(麦克风的电流音?)、音量严重降低(被切换到听筒播放?)等奇形怪状的问题,因此如果你录音后有别的操作,尽量录完音就立即调用close关闭录音。** +`fail`=fn(errMsg,isUserNotAllow); 如果是用户主动拒绝的录音权限,除了有错误消息外,isUserNotAllow=true,方便程序中做不同的提示,提升用户主动授权概率 -> **特别注**: 鉴于UC系浏览器(大部分老旧国产手机厂商系统浏览器)大概率表面支持录音但永远不会有任何回调、或者此浏览器支持第三种情况(用户忽略 并且 此浏览器认为此种情况不需要回调 并且程序员完美实现了);如果当前环境是移动端,可以在调用此方法`8秒`后如果未收到任何回调,弹出一个自定义提示框(只需要一个按钮),提示内容范本:`录音功能需要麦克风权限,请允许;如果未看到任何请求,请点击忽略~`,按钮文本:`忽略`;当用户点击了按钮,直接手动执行`fail`逻辑,因为此时浏览器压根就没有弹移动端特有的模态化权限请求对话框;但如果收到了回调(可能是同步的,因此弹框必须在`rec.open`调用前准备好随时取消),需要把我们弹出的提示框自动关掉,不需要用户做任何处理。pc端的由于不是模态化的请求对话框,可能会被用户误点,所以尽量要判断一下是否是移动端。 +注意:此方法回调是可能是同步的(异常、或者已持有资源时)也可能是异步的(浏览器弹出权限请求时);一般使用时打开,用完立即关闭;可重复调用,可用来测试是否能录音。 -`success`=fn(); +另外:普通的麦克风录音时,因为此方法会调起用户授权请求,如果仅仅想知道浏览器是否支持录音(比如:如果浏览器不支持就走另外一套录音方案),应使用`Recorder.Support()`方法。 -`fail`=fn(errMsg,isUserNotAllow); 如果是用户主动拒绝的录音权限,除了有错误消息外,isUserNotAllow=true,方便程序中做不同的提示,提升用户主动授权概率 +> **特别注**: 鉴于UC系浏览器(大部分老旧国产手机厂商系统浏览器)大概率表面支持录音但永远不会有任何回调、或者此浏览器支持第三种情况(忽略按钮,也不会有回调);如果当前环境是移动端,可以在调用此方法`8秒`后如果未收到任何回调,弹出一个自定义提示框(只需要一个按钮),提示内容范本:`录音功能需要麦克风权限,请允许;如果未看到任何请求,请点击忽略~`,按钮文本:`忽略`;当用户点击了按钮,直接手动执行`fail`逻辑,因为此时浏览器压根就没有弹移动端特有的模态化权限请求对话框;但如果收到了回调(可能是同步的,因此弹框必须在`rec.open`调用前准备好随时取消),需要把我们弹出的提示框自动关掉,不需要用户做任何处理。pc端的由于不是模态化的请求对话框,可能会被用户误点,所以尽量要判断一下是否是移动端。 ### 【方法】rec.close(success) @@ -655,7 +645,7 @@ function transformOgg(pcmData){ 设置为空字符串后将不参与统计,大部分情况下无需关闭统计,如果你网页的url私密性要求很高,请在调用Recorder之前将此url设为空字符串;本功能于2019-11-09添加,[点此](https://www.51.la/?20469973)前往51la查看统计概况。 ### 【静态属性】Recorder.BufferSize -普通的麦克风录音时全局的AudioContext缓冲大小,默认值为4096。会影响H5录音时的onProcess调用速率,相对于AudioContext.sampleRate=48000时,4096接近12帧/s,调节此参数可生成比较流畅的回调动画。 +普通的麦克风录音时全局的AudioContext缓冲大小,默认值为4096。会影响H5录音时的onProcess调用速率,相对于AudioContext.sampleRate=48000时,4096接近12帧/s(移动端帧率可能会低一些),调节此参数可生成比较流畅的回调动画。 取值256, 512, 1024, 2048, 4096, 8192, or 16384 @@ -665,8 +655,18 @@ function transformOgg(pcmData){ *这个属性在旧版Recorder中是放在已废弃的set.bufferSize中,后面因为兼容处理Safari上MediaStream断开后就无法再次进行连接使用的问题(表现为静音),把MediaStream连接也改成了全局只连接一次,因此set.bufferSize就移出来变成了Recorder的属性* +### 【静态属性】Recorder.ConnectEnableWebM +音频采集连接方式:启用时尝试使用MediaRecorder.WebM.PCM,默认为true启用,未启用或者不支持时使用AudioWorklet或过时的ScriptProcessor来连接;本连接方式仅对普通麦克风录音时有效,直接提供了流(set.sourceStream)时将当做未启用处理。 + +使用MediaRecorder采集到的音频数据比其他方式更好,几乎不存在丢帧现象,所以音质明显会好很多,建议保持开启; 有些浏览器不支持录制PCM编码的WebM,如FireFox、低版本的Chrome,将依旧使用AudioWorklet或ScriptProcessor来连接采集。 + +*可以额外提供一个设置`Recorder.ConnectWebMOptions={}`来当做MediaRecorder的options参数,支持的参数请参考[此文档](https://developer.mozilla.org/zh-CN/docs/Web/API/MediaRecorder/MediaRecorder)。* + +> 本连接实现原理:通过MediaRecorder对MediaStream进行录制,格式`audio/webm; codecs=pcm`,MediaRecorder会将实时录制的PCM数据(48k+32位)回传给js,因此只需要知道WebM的封装格式就能提取出PCM数据,请参考测试代码:[WebM格式解析并提取音频](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.webm.extract_audio) + + ### 【静态属性】Recorder.ConnectEnableWorklet -音频采集连接方式:是否要启用AudioWorklet (AudioWorkletNode) 来进行连接;默认为false禁用,禁用后将使用过时的ScriptProcessor (AudioContext.createScriptProcessor) 来连接。 +音频采集连接方式:是否要启用AudioWorklet (AudioWorkletNode) 来进行连接;默认为false禁用,禁用后将使用过时的ScriptProcessor (AudioContext.createScriptProcessor) 来连接;如果启用了 Recorder.ConnectEnableWebM 并且有效时,本参数将不起作用,否则才会生效。 启用后如果浏览器不支持AudioWorklet,将只会使用老的ScriptProcessor来进行音频采集连接;如果浏览器已停止支持ScriptProcessor,将永远会尝试启用AudioWorklet而忽略此配置值。 @@ -758,7 +758,7 @@ function transformOgg(pcmData){ ## WaveView插件 [waveview.js](https://github.com/xiangyuecn/Recorder/blob/master/src/extensions/waveview.js),4kb大小源码,录音时动态显示波形,具体样子参考演示地址页面。此插件参考[MCVoiceWave](https://github.com/HaloMartin/MCVoiceWave)库编写的,具体代码在`https://github.com/HaloMartin/MCVoiceWave/blob/f6dc28975fbe0f7fc6cc4dbc2e61b0aa5574e9bc/MCVoiceWave/MCVoiceWaveView.m`中;本可视化插件可以移植到其他语言环境,如需定制可联系作者。 -此插件是在录音时`onProcess`回调中使用;`Recorder.BufferSize`会影响绘制帧率,默认配置的大概12帧/s比较流畅。基础使用方法:[](?Ref=WaveView.Codes&Start) +此插件是在录音时`onProcess`回调中使用。基础使用方法:[](?Ref=WaveView.Codes&Start) ``` javascript var wave; var rec=Recorder({ @@ -787,7 +787,11 @@ set={ //以上配置二选一 ,scale:2 //缩放系数,应为正整数,使用2(3? no!)倍宽高进行绘制,避免移动端绘制模糊 - ,speed:8 //移动速度系数,越大越快 + ,speed:9 //移动速度系数,越大越快 + ,phase:21.8 //相位,调整了速度后,调整这个值得到一个看起来舒服的波形 + + ,fps:20 //绘制帧率,调整后也需调整phase值 + ,keep:true //当停止了input输入时,是否保持波形,设为false停止后将变成一条线 ,lineWidth:3 //线条基础粗细 @@ -799,7 +803,7 @@ set={ ``` ### 【方法】wave.input(pcmData,powerLevel,sampleRate) -输入音频数据,更新波形显示,这个方法调用的越快,波形越流畅。pcmData `[Int16,...]` 一维数组,为当前的录音数据片段,其他参数和`onProcess`回调相同。 +输入音频数据,更新波形显示。pcmData `[Int16,...]` 一维数组,为当前的录音数据片段,其他参数和`onProcess`回调相同。 @@ -1276,7 +1280,7 @@ wav格式编码器时参考网上资料写的,会发现代码和别人家的 由于RAW格式的wav内直接就是pcm数据,因此将小的wav片段文件去掉wav头后得到的原始pcm数据合并到一起,再加上新的wav头即可合并出长的wav文件;要求待合成的所有wav片段的采样率和位数需一致。[wav合并参考和测试+可移植源码](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.merge.wav_merge) ## mp3 (CBR) 格式 -采用的是[lamejs](https://github.com/zhuker/lamejs)(LGPL License)这个库的代码,`https://github.com/zhuker/lamejs/blob/bfb7f6c6d7877e0fe1ad9e72697a871676119a0e/lame.all.js`这个版本的文件代码;已对lamejs源码进行了部分改动,用于精简代码和修复发现的问题。LGPL协议涉及到的文件:`mp3-engine.js`;这些文件也采用LGPL授权,不适用MIT协议。源码518kb大小,压缩后150kb左右,开启gzip后50来k。[mp3转其他格式参考和测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other) +采用的是[lamejs](https://github.com/zhuker/lamejs)(LGPL License)这个库的代码,`https://github.com/zhuker/lamejs/blob/bfb7f6c6d7877e0fe1ad9e72697a871676119a0e/lame.all.js`这个版本的文件代码;已对lamejs源码进行了部分改动,用于精简代码和修复发现的问题。LGPL协议涉及到的文件:`mp3-engine.js`;这些文件也采用LGPL授权,不适用MIT协议。源码518kb大小,压缩后160kb左右,开启gzip后60来k。[mp3转其他格式参考和测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other) ### 简单将多段小的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.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.merge.mp3_merge) diff --git a/assets/demo-ts/dist/index.js b/assets/demo-ts/dist/index.js new file mode 100644 index 0000000..9523dce --- /dev/null +++ b/assets/demo-ts/dist/index.js @@ -0,0 +1 @@ +!function(e){var t={};function a(s){if(t[s])return t[s].exports;var n=t[s]={i:s,l:!1,exports:{}};return e[s].call(n.exports,n,n.exports,a),n.l=!0,n.exports}a.m=e,a.c=t,a.d=function(e,t,s){a.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},a.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},a.t=function(e,t){if(1&t&&(e=a(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var s=Object.create(null);if(a.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)a.d(s,n,function(t){return e[t]}.bind(null,n));return s},a.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return a.d(t,"a",t),t},a.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},a.p="",a(a.s=0)}([function(e,t,a){"use strict";var s=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0});var n=s(a(1));a(2),a(3),a(4);var r,i,o,_=window,l=document,f=function(){for(var e=[],t=0;t
0;){var s;0==p&&(p=8,u++,l[u]=0),a-=s=Math.min(a,p),p-=s,l[u]|=t>>a<
=8&&(b(s,76,8),t-=8),t>=8&&(b(s,65,8),t-=8),t>=8&&(b(s,77,8),t-=8),t>=8&&(b(s,69,8),t-=8),t>=32){var n=i.getLameShortVersion();if(t>=32)for(a=0;a15){if(u>14)h|=u-15<<1,l=f,u=15;if(p>14)h<<=f,h|=p-15,l+=f,p=15;c=16}0!=p&&(h<<=1,n.xr[o+1]<0&&h++,_--),u=u*c+p,l-=_,_+=r.hlen[u],b(e,r.table[u],_),b(e,h,l),i+=_+l}return i}function A(e,t){var a=3*e.scalefac_band.s[3];a>t.big_values&&(a=t.big_values);var s=k(e,t.table_select[0],0,a,t);return s+=k(e,t.table_select[1],a,t.big_values,t)}function y(e,t){var a,s,n,r;a=t.big_values;var i=t.region0_count+1;return n=e.scalefac_band.l[i],i+=t.region1_count+1,n>a&&(n=a),(r=e.scalefac_band.l[i])>a&&(r=a),s=k(e,t.table_select[0],0,n,t),s+=k(e,t.table_select[1],n,r,t),s+=k(e,t.table_select[2],r,a,t)}function T(){this.total=0}function x(t,a){var s,n,r,i,o,_=t.internal_flags;return o=_.w_ptr,-1==(i=_.h_ptr-1)&&(i=Z.MAX_HEADER_BUF-1),s=_.header[i].write_timing-f,a.total=s,s>=0&&(n=1+i-o,i=s?e.ATH.adjust=s:e.ATH.adjust=o)break;S=w-h.itime[l]-(u+d%2*.5);R=0|Math.floor(2*S*p+p+.5);var k=0;for(f=0;f<=d;++f){var A=f+u-d/2;k+=(A<0?M[v+A]:n[i+A])*h.blackfilt[R][f]}t[a+c]=k}if(_.num_used=Math.min(o,d+u-d/2),h.itime[l]+=_.num_used-c*h.resample_ratio,_.num_used>=v)for(f=0;f
+> npm install
+> npm run build-dev
+
+ 然后就可以打开index.html查看效果了。\n> cnpm install\n> npm run build-dev\n
\n 然后就可以打开index.html查看效果了。parseInt(this.max)&&Cn(s,o[0],o,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};!function(e){var t={get:function(){return V}};Object.defineProperty(e,"config",t),e.util={warn:ue,extend:C,mergeOptions:De,defineReactive:Be},e.set=Ee,e.delete=Ce,e.nextTick=at,e.observable=function(e){return Te(e),e},e.options=Object.create(null),D.forEach((function(t){e.options[t+"s"]=Object.create(null)})),e.options._base=e,C(e.options.components,On),function(e){e.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(t.indexOf(e)>-1)return this;var n=E(arguments,1);return n.unshift(this),"function"==typeof e.install?e.install.apply(e,n):"function"==typeof e&&e.apply(null,n),t.push(e),this}}(e),function(e){e.mixin=function(e){return this.options=De(this.options,e),this}}(e),Rn(e),function(e){D.forEach((function(t){e[t]=function(e,n){return n?("component"===t&&u(n)&&(n.name=n.name||e,n=this.options._base.extend(n)),"directive"===t&&"function"==typeof n&&(n={bind:n,update:n}),this.options[t+"s"][e]=n,n):this.options[t+"s"][e]}}))}(e)}(Mn),Object.defineProperty(Mn.prototype,"$isServer",{get:se}),Object.defineProperty(Mn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Mn,"FunctionalRenderContext",{value:It}),Mn.version="2.6.10";var Ln=m("style,class"),In=m("input,textarea,option,select,progress"),Hn=function(e,t,n){return"value"===n&&In(e)&&"button"!==t||"selected"===n&&"option"===e||"checked"===n&&"input"===e||"muted"===n&&"video"===e},Nn=m("contenteditable,draggable,spellcheck"),$n=m("events,caret,typing,plaintext-only"),Dn=m("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,translate,truespeed,typemustmatch,visible"),jn="http://www.w3.org/1999/xlink",Vn=function(e){return":"===e.charAt(5)&&"xlink"===e.slice(0,5)},Fn=function(e){return Vn(e)?e.slice(6,e.length):""},qn=function(e){return null==e||!1===e};function Yn(e){for(var t=e.data,n=e,r=e;s(r.componentInstance);)(r=r.componentInstance._vnode)&&r.data&&(t=Xn(r.data,t));for(;s(n=n.parent);)n&&n.data&&(t=Xn(t,n.data));return function(e,t){if(s(e)||s(t))return zn(e,Un(t));return""}(t.staticClass,t.class)}function Xn(e,t){return{staticClass:zn(e.staticClass,t.staticClass),class:s(e.class)?[e.class,t.class]:t.class}}function zn(e,t){return e?t?e+" "+t:e:t||""}function Un(e){return Array.isArray(e)?function(e){for(var t,n="",r=0,a=e.length;r-1?br(e,t,n):Dn(t)?qn(n)?e.removeAttribute(t):(n="allowfullscreen"===t&&"EMBED"===e.tagName?"true":t,e.setAttribute(t,n)):Nn(t)?e.setAttribute(t,function(e,t){return qn(t)||"false"===t?"false":"contenteditable"===e&&$n(t)?t:"true"}(t,n)):Vn(t)?qn(n)?e.removeAttributeNS(jn,Fn(t)):e.setAttributeNS(jn,t,n):br(e,t,n)}function br(e,t,n){if(qn(n))e.removeAttribute(t);else{if(W&&!J&&"TEXTAREA"===e.tagName&&"placeholder"===t&&""!==n&&!e.__ieph){e.addEventListener("input",(function t(n){n.stopImmediatePropagation(),e.removeEventListener("input",t)})),e.__ieph=!0}e.setAttribute(t,n)}}var gr={create:vr,update:vr};function yr(e,t){var n=t.elm,r=t.data,a=e.data;if(!(i(r.staticClass)&&i(r.class)&&(i(a)||i(a.staticClass)&&i(a.class)))){var o=Yn(t),l=n._transitionClasses;s(l)&&(o=zn(o,Un(l))),o!==n._prevClass&&(n.setAttribute("class",o),n._prevClass=o)}}var wr,Sr,xr,kr,Ar,Mr,Rr={create:yr,update:yr},Tr=/[\w).+\-_$\]]/;function Br(e){var t,n,r,a,i,s=!1,o=!1,l=!1,c=!1,f=0,u=0,_=0,p=0;for(r=0;r0&&(n="transition",f=s,u=i.length):"animation"===t?c>0&&(n="animation",f=c,u=l.length):u=(n=(f=Math.max(s,c))>0?s>c?"transition":"animation":null)?"transition"===n?i.length:l.length:0,{type:n,timeout:f,propCount:u,hasTransform:"transition"===n&&Oa.test(r[ka+"Property"])}}function Ia(e,t){for(;e.length\n> cnpm install\n> npm run build-dev\n
\n 然后就可以打开index.html查看效果了。0&&(u+=o.buffer.byteLength,c.push(o.buffer)),f+=57600,setTimeout(n)}else{var o;(o=l.flush()).length>0&&(u+=o.buffer.byteLength,c.push(o.buffer));var _=a.fn(c,u,s,r.sampleRate);i(_,r),t(new Blob(c,{type:"audio/mp3"}))}}()},Recorder.BindDestroy("mp3Worker",(function(){console.log("mp3Worker Destroy"),e&&e.terminate(),e=null})),Recorder.prototype.mp3_envCheck=function(e,t){var r="";return t.takeoffEncodeChunk&&(e.canProcess?n()||(r="当前浏览器版本太低,无法实时处理"):r=e.envName+"环境不支持实时处理"),r},Recorder.prototype.mp3_start=function(e){return n(e)};var t={id:0},n=function(n){var r=e;try{if(!r){var i=");wk_lame();var wk_ctxs={};self.onmessage="+function(e){var t=e.data,n=wk_ctxs[t.id];if("init"==t.action)wk_ctxs[t.id]={sampleRate:t.sampleRate,bitRate:t.bitRate,takeoff:t.takeoff,mp3Size:0,pcmSize:0,encArr:[],encObj:new wk_lame.Mp3Encoder(1,t.sampleRate,t.bitRate)};else if(!n)return;switch(t.action){case"stop":n.encObj=null,delete wk_ctxs[t.id];break;case"encode":n.pcmSize+=t.pcm.length,(r=n.encObj.encodeBuffer(t.pcm)).length>0&&(n.takeoff?self.postMessage({action:"takeoff",id:t.id,chunk:r}):(n.mp3Size+=r.buffer.byteLength,n.encArr.push(r.buffer)));break;case"complete":var r;(r=n.encObj.flush()).length>0&&(n.takeoff?self.postMessage({action:"takeoff",id:t.id,chunk:r}):(n.mp3Size+=r.buffer.byteLength,n.encArr.push(r.buffer)));var a=wk_mp3TrimFix.fn(n.encArr,n.mp3Size,n.pcmSize,n.sampleRate);self.postMessage({action:t.action,id:t.id,blob:new Blob(n.encArr,{type:"audio/mp3"}),meta:a})}};i+=";var wk_mp3TrimFix={rm:"+a.rm+",fn:"+a.fn+"}";var s=Recorder.lamejs.toString(),o=(window.URL||webkitURL).createObjectURL(new Blob(["var wk_lame=(",s,i],{type:"text/javascript"}));r=new Worker(o),setTimeout((function(){(window.URL||webkitURL).revokeObjectURL(o)}),1e4),r.onmessage=function(e){var n=e.data,r=t[n.id];r&&("takeoff"==n.action?r.set.takeoffEncodeChunk(new Uint8Array(n.chunk.buffer)):(r.call&&r.call(n),r.call=null))}}var l={worker:r,set:n,takeoffQueue:[]};return n?(l.id=++t.id,t[l.id]=l,r.postMessage({action:"init",id:l.id,sampleRate:n.sampleRate,bitRate:n.bitRate,takeoff:!!n.takeoffEncodeChunk,x:new Int16Array(5)})):r.postMessage({x:new Int16Array(5)}),e=r,l}catch(e){return r&&r.terminate(),console.error(e),null}};Recorder.prototype.mp3_stop=function(e){if(e&&e.worker){e.worker.postMessage({action:"stop",id:e.id}),e.worker=null,delete t[e.id];var n=-1;for(var r in t)n++;n&&console.warn("mp3 worker剩"+n+"个在串行等待")}},Recorder.prototype.mp3_encode=function(e,t){e&&e.worker&&e.worker.postMessage({action:"encode",id:e.id,pcm:t})},Recorder.prototype.mp3_complete=function(e,t,n,r){var a=this;e&&e.worker?(e.call=function(n){i(n.meta,e.set),t(n.blob),r&&a.mp3_stop(e)},e.worker.postMessage({action:"complete",id:e.id})):n("mp3编码器未打开")},Recorder.mp3ReadMeta=function(e,t){var n="object"==("undefined"==typeof window?"undefined":r(window))?window.parseInt:self.parseInt,a=new Uint8Array(e[0]||[]);if(a.length<4)return null;var i=function(e,t){return("0000000"+((t||a)[e]||0).toString(2)).substr(-8)},s=i(0)+i(1),o=i(2)+i(3);if(!/^1{11}/.test(s))return null;var l={"00":2.5,10:2,11:1}[s.substr(11,2)],c={"01":3}[s.substr(13,2)],f={1:[44100,48e3,32e3],2:[22050,24e3,16e3],2.5:[11025,12e3,8e3]}[l];f&&(f=f[n(o.substr(4,2),2)]);var u=[[0,8,16,24,32,40,48,56,64,80,96,112,128,144,160],[0,32,40,48,56,64,80,96,112,128,160,192,224,256,320]][1==l?1:0][n(o.substr(0,4),2)];if(!(l&&c&&u&&f))return null;for(var _=Math.round(8*t/u),p=1==c?384:2==c||1==l?1152:576,h=p/f*1e3,d=Math.floor(p*u/8/f*1e3),v=0,m=0,b=0;bl)return I.LARGE_BITS;if(function(t,n,a,s,o){var l,c,f,_=0,p=0,h=0,d=0,v=n,m=0,b=v,g=0,y=t,w=0;for(f=null!=o&&s.global_gain==o.global_gain,c=s.block_type==F.SHORT_TYPE?38:21,l=0;l<=c;l++){var S=-1;if((f||s.block_type==F.NORM_TYPE)&&(S=s.global_gain-(s.scalefac[l]+(0!=s.preflag?e.pretab[l]:0)<