diff --git a/README.md b/README.md index c5a0f21..f86006f 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ [​](?) -录音默认输出mp3格式,另外可选wav、pcm格式;有限支持ogg(beta)、webm(beta)、amr(beta)格式;支持任意格式扩展(前提有相应编码器)。 +录音默认输出mp3格式,另外可选wav、pcm、g711a、g711u格式;有限支持ogg(beta)、webm(beta)、amr(beta)格式;支持任意格式扩展(前提有相应编码器)。 -> mp3默认16kbps的比特率,2kb每秒的录音大小,音质还可以(如果使用8kbps可达到1kb每秒,不过音质太渣)。主要用于语音录制,双声道语音没有意义,特意仅对单声道进行支持。mp3、wav、pcm格式支持边录边转码,录音结束时转码速度极快,支持实时转码成小片段文件和实时传输,demo中已实现一个语音通话聊天,下面有介绍;其他格式录音结束时可能需要花费比较长的时间进行转码。 +> mp3默认16kbps的比特率,2kb每秒的录音大小,音质还可以(如果使用8kbps可达到1kb每秒,不过音质太渣)。主要用于语音录制,双声道语音没有意义,特意仅对单声道进行支持。mp3、wav、pcm、g711a、g711u格式支持边录边转码,录音结束时转码速度极快,支持实时转码成小片段文件和实时传输,demo中已实现一个语音通话聊天,下面有介绍;其他格式录音结束时可能需要花费比较长的时间进行转码。 > > mp3使用lamejs编码(CBR),压缩后的recorder.mp3.min.js文件160kb左右(开启gzip后60kb)。如果对录音文件大小没有特别要求,可以仅仅使用录音核心+wav编码器(raw pcm format录音文件超大),压缩后的recorder.wav.min.js不足20kb。录音得到的mp3(CBR)、wav(PCM),均可简单拼接小的二进制录音片段文件来生成长的音频文件,具体参考下面这两种编码器的详细介绍。 @@ -89,7 +89,8 @@ 17. [【Demo库】【信号处理】IIR低通、高通滤波](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=dsp.lib.filter.iir) 18. [【测试】【信号处理】FFT频域分析ECharts频谱曲线图](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=dsp.test.fft.analysis) 19. [【测试】WebM格式解析并提取音频](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.webm.extract_audio) -20. [【测试】音频可视化相关插件测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.extensions.visualization) +20. [【测试】G711、G72X编码和解码播放](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.g7xx.engine) +21. [【测试】音频可视化相关插件测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=test.extensions.visualization) ### App Demo @@ -157,7 +158,8 @@ iOS Demo App :[下载源码](https://github.com/xiangyuecn/Recorder/tree/maste > https环境搭建最佳实践:建议给自己的域名申请一个泛域名通配符证书(*.xxx.com),然后线上、本地开发均可使用此证书;本地开发环境直接分配一个三级域名(dev.xxx.com、local.xxx.com、192-168-1-123.xxx.com)解析A记录到电脑局域网的IP地址(192.168.1.123、127.0.0.1),方便本地开发跨端调试(本地如何配置https请针对自己的开发环境自行搜索,很容易)。 > > 获取泛域名通配符证书推荐:[在线免费申请(ZeroSSL、Let’s Encrypt)](https://xiangyuecn.gitee.io/acme-html-web-browser-client/ACME-HTML-Web-Browser-Client.html);不建议自己生成根证书来签发域名证书,一个是流程复杂,每个设备均要导入根证书,致命的是很多现代浏览器不再信任用户目录下导入的根证书(Android)。 - +> +> 如果必须http访问,Chrome中可尝试打开`chrome://flags/#unsafely-treat-insecure-origin-as-secure`,启用`Insecure origins treated as secure`,把你的地址含端口配置进去。 ## 【1】加载框架 @@ -437,6 +439,8 @@ App端建议使用原生插件来录音,没有这些框架缺陷带来的性 浏览器Audio Media[兼容性](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility)mp3最好,wav还行,其他要么不支持播放,要么不支持编码;因此本库最佳推荐使用mp3、wav格式,代码也是优先照顾这两种格式。 +`getUserMedia`的浏览器支持情况和兼容性,可到[Can I use](https://caniuse.com/?search=getUserMedia)查看。 + **留意中途来电话**:在移动端录音时,如果录音中途来电话,或者通话过程中打开录音,是不一定能进行录音的;经过简单测试发现,iOS上Safari将暂停返回音频数据,直到通话结束才开始继续有音频数据返回;小米上Chrome不管是来电还是通话中开始录音都能对麦克风输入的声音进行录音;只是简单测试,更多机器和浏览器并未做测试,不过整体上来看来电话或通话中进行录音的可行性并不理想,也不赞成在这种过程中进行录音;但只要通话结束后录音还是会正常进行,影响基本不大。 @@ -734,6 +738,9 @@ function transformOgg(pcmData){ index:0 pcmDatas已处理到的索引 offset:0.0 已处理到的index对应的pcm中的偏移的下一个位置 + //可定义,指定的一个滤波配置:默认使用Recorder.IIRFilter低通滤波(可有效抑制混叠产生的杂音,新采样率大于pcm采样率的75%时不默认滤波),如果提供了配置但fn为null时将不滤波;sr为此滤波函数对应的初始化采样率,当采样率和pcmSampleRate参数不一致时将重新设为默认函数 + filter:null||{fn:fn(sample),sr:pcmSampleRate} + //仅作为返回值 frameNext:null||[Int16,...] 下一帧的部分数据,frameSize设置了的时候才可能会有 sampleRate:16000 结果的采样率,<=newSampleRate @@ -741,6 +748,19 @@ function transformOgg(pcmData){ } ``` + +### 【静态方法】Recorder.IIRFilter(useLowPass,sampleRate,freq) +IIR低通、高通滤波;可重新赋值一个函数,来改变Recorder的默认行为,比如SampleData中的低通滤波。 + +`useLowPass`: true或false,true为低通滤波,false为高通滤波 + +`sampleRate`: 待处理pcm的采样率 + +`freq`: 截止频率Hz,最大频率为sampleRate/2,低通时会切掉高于此频率的声音,高通时会切掉低于此频率的声音,注意滤波并非100%的切掉不需要的声音,而是减弱频率对应的声音,离截止频率越远对应声音减弱越厉害,离截止频率越近声音就几乎无衰减 + +返回的是一个函数,用此函数对pcm的每个采样值按顺序进行处理即可(不同pcm不可共用)。 + + ### 【静态方法】Recorder.PowerLevel(pcmAbsSum,pcmLength) 计算音量百分比的一个方法,返回值:0-100,主要当做百分比用;注意:这个不是分贝,因此没用volume当做名称。 @@ -1296,26 +1316,26 @@ EncodeMix对象: [​](?) # :open_book:已有的音频格式编码器 -如果你有其他格式的编码器并且想贡献出来,可以提交新增格式文件的PR(文件放到/src/engine中),我们升级它。 +所有音频格式的编码器都在`/src/engine`目录中,每个格式一般有一个同名的js文件,如果这个格式有额外的编码引擎(`*-engine.js`)的话,使用时必须要一起加上。 ## pcm 格式 -pcm编码器输出的数据其实就是Recorder中的buffers原始数据(经过了重新采样),16位时为LE小端模式(Little Endian),并未经过任何编码处理;pcm为未封装的原始音频数据,pcm数据文件无法直接播放,pcm加上一个44字节wav头即成wav文件,可通过wav格式来正常播放。两个参数相同的pcm文件直接二进制拼接在一起即可成为长的pcm文件,[pcm片段文件合并+可移植源码:PCMMerge](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer_frame_pcm)。 +依赖文件:`pcm.js`,pcm编码器输出的数据其实就是Recorder中的buffers原始数据(经过了重新采样),16位时为LE小端模式(Little Endian),并未经过任何编码处理;pcm为未封装的原始音频数据,pcm数据文件无法直接播放,pcm加上一个44字节wav头即成wav文件,可通过wav格式来正常播放。两个参数相同的pcm文件直接二进制拼接在一起即可成为长的pcm文件,[pcm片段文件合并+可移植源码:PCMMerge](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=teach.realtime.encode_transfer_frame_pcm)。 ### Recorder.pcm2wav(data,True,False) 已实现的一个把pcm转成wav格式来播放的方法,`data = { sampleRate:16000 pcm的采样率 , bitRate:16 pcm的位数 取值:8 或 16 , blob:pcm的blob对象 }`,`True=fn(wavBlob,duration)`。要使用此方法需要带上`wav`格式编码器。 ## wav (raw pcm format) 格式 -wav格式编码器时参考网上资料写的,会发现代码和别人家的差不多。源码2kb大小。[wav转其他格式参考和测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.wav2other) +依赖文件:`wav.js`,wav格式编码器时参考网上资料写的,会发现代码和别人家的差不多。源码2kb大小。[wav转其他格式参考和测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.wav2other) ### wav转pcm -生成的wav文件内音频数据的编码为未压缩的pcm数据(raw pcm),只是在pcm数据前面加了一个44字节的wav头;因此直接去掉前面44字节就能得到原始的pcm数据,如:`blob.slice(44,blob.size,"audio/pcm")`; +生成的wav文件内音频数据的编码为未压缩的pcm数据(raw pcm),只是在pcm数据前面加了一个44字节的wav头;因此直接去掉前面44字节就能得到原始的pcm数据,如:`blob.slice(44,blob.size,"audio/pcm")`;注意:其他wav编码器可能不是44字节的头,要从任意wav文件中提取pcm数据,请参考:`assets/runtime-codes/fragment.decode.wav.js`。 ### 简单将多段小的wav片段合成长的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大小,压缩后160kb左右,开启gzip后60来k。[mp3转其他格式参考和测试](https://xiangyuecn.gitee.io/recorder/assets/工具-代码运行和静态分发Runtime.html?jsname=lib.transform.mp32other) +依赖文件:`mp3.js + mp3-engine.js`,采用的是[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) @@ -1323,14 +1343,24 @@ wav格式编码器时参考网上资料写的,会发现代码和别人家的 *注:CBR编码由于每帧数据的时长是固定的,mp3文件结尾最后这一帧的录音可能不能刚好填满,就会产生填充数据,多出来的这部分数据会导致mp3时长变长一点点,在实时转码传输时应当留意,解码成pcm后可直接去掉结尾的多余;另外可以通过调节待编码的pcm数据长度以达到刚好填满最后一帧来规避此问题,参考`Recorder.SampleData`方法提供的连续转码针对此问题的处理。首帧或前两帧可能是lame记录的信息帧,本库已去除(但小的mp3片段拼接起来停顿导致的杂音还是非常明显,实时处理时使用`takeoffEncodeChunk`选项可完全避免此问题),参考上面的已知问题。* +## g711a g711u 格式 +依赖文件:`g711x.js`,g711a: G.711 A-law (pcma),g711u: G.711 μ-law (pcmu、mu-law);支持g711的编码和解码,编解码源码移植自:`https://github.com/twstx1/codec-for-audio-in-G72X-G711-G723-G726-G729/blob/master/G711_G721_G723/g711.c`;固定为8000hz采样率、16位,每个采样压缩成8位存储,音频文件大小为8000字节/秒。 + +### Recorder.g711a_decode|g711u_decode(bytes) +解码g711x得到pcm,`bytes`: Uint8Array,g711x二进制数据;返回Int16Array,为8000采样率、16位的pcm数据。 + +### Recorder.g711a2wav|g711u2wav(g711xBlob,True,False) +已实现把g711a、g711u转成wav格式来播放的方法,`g711xBlob为g711x音频文件blob对象`,`True=fn(wavBlob,duration)`;要使用此方法需要带上`wav`格式编码器。 + + ## beta-ogg (Vorbis) 格式 -采用的是[ogg-vorbis-encoder-js](https://github.com/higuma/ogg-vorbis-encoder-js)(MIT License),`https://github.com/higuma/ogg-vorbis-encoder-js/blob/7a872423f416e330e925f5266d2eb66cff63c1b6/lib/OggVorbisEncoder.js`这个版本的文件代码。此编码器源码2.2M,超级大,压缩后1.6M,开启gzip后327K左右。对录音的压缩率比lamejs高出一倍, 但Vorbis in Ogg好像Safari不支持([真的假的](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility))。 +依赖文件:`beta-ogg.js + beta-ogg-engine.js`,采用的是[ogg-vorbis-encoder-js](https://github.com/higuma/ogg-vorbis-encoder-js)(MIT License),`https://github.com/higuma/ogg-vorbis-encoder-js/blob/7a872423f416e330e925f5266d2eb66cff63c1b6/lib/OggVorbisEncoder.js`这个版本的文件代码。此编码器源码2.2M,超级大,压缩后1.6M,开启gzip后327K左右。对录音的压缩率比lamejs高出一倍, 但Vorbis in Ogg好像Safari不支持([真的假的](https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats#Browser_compatibility))。 ## beta-webm 格式 -这个编码器时通过查阅MDN编写的一个玩意,没多大使用价值:录几秒就至少要几秒来编码。。。原因是:未找到对已有pcm数据进行快速编码的方法。数据导入到MediaRecorder,音频有几秒就要等几秒,类似边播放边收听形。(想接原始录音Stream?我不可能给的!)输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞。只有比较新的浏览器支持(需实现浏览器MediaRecorder),压缩率和mp3差不多。源码2kb大小。 +依赖文件:`beta-webm.js`,这个编码器时通过查阅MDN编写的一个玩意,没多大使用价值:录几秒就至少要几秒来编码。。。原因是:未找到对已有pcm数据进行快速编码的方法。数据导入到MediaRecorder,音频有几秒就要等几秒,类似边播放边收听形。(想接原始录音Stream?我不可能给的!)输出音频虽然可以通过比特率来控制文件大小,但音频文件中的比特率并非设定比特率,采样率由于是我们自己采样的,到这个编码器随他怎么搞。只有比较新的浏览器支持(需实现浏览器MediaRecorder),压缩率和mp3差不多。源码2kb大小。 ## beta-amr (NB 窄带) 格式 -采用的是[benz-amr-recorder](https://github.com/BenzLeung/benz-amr-recorder)(MIT License)优化后的[amr.js](https://github.com/jpemartins/amr.js)(Unknown License),`https://github.com/BenzLeung/benz-amr-recorder/blob/462c6b91a67f7d9f42d0579fb5906fad9edb2c9d/src/amrnb.js`这个版本的文件代码,已对此代码进行过调整更方便使用。支持编码和解码操作。由于最高只有12.8kbps的码率(AMR 12.2,8000hz),音质和同比配置的mp3、ogg差一个档次。由于支持解码操作,理论上所有支持Audio的浏览器都可以播放(需要自己写代码实现)。源码1M多,蛮大,压缩后445K,开启gzip后136K。优点:录音文件小。 +依赖文件:`beta-amr.js + beta-amr-engine.js`,采用的是[benz-amr-recorder](https://github.com/BenzLeung/benz-amr-recorder)(MIT License)优化后的[amr.js](https://github.com/jpemartins/amr.js)(Unknown License),`https://github.com/BenzLeung/benz-amr-recorder/blob/462c6b91a67f7d9f42d0579fb5906fad9edb2c9d/src/amrnb.js`这个版本的文件代码,已对此代码进行过调整更方便使用。支持编码和解码操作。由于最高只有12.8kbps的码率(AMR 12.2,8000hz),音质和同比配置的mp3、ogg差一个档次。由于支持解码操作,理论上所有支持Audio的浏览器都可以播放(需要自己写代码实现)。源码1M多,蛮大,压缩后445K,开启gzip后136K。优点:录音文件小。 ### Recorder.amr2wav(amrBlob,True,False) 已实现的一个把amr转成wav格式来播放的方法,`True=fn(wavBlob,duration)`。要使用此方法需要带上上面的`wav`格式编码器。仿照此方法可轻松转成别的格式,参考`mock`方法介绍那节。 diff --git a/app-support-sample/index.html b/app-support-sample/index.html index 97d486c..264caaa 100644 --- a/app-support-sample/index.html +++ b/app-support-sample/index.html @@ -273,6 +273,8 @@ + + @@ -986,8 +988,7 @@ var end=function(){ var enc=Recorder.prototype["enc_"+type]; var tips=[!enc?"这个编码器无提示信息":type+"编码器"+(enc.stable?"稳定版":"beta版")+",wav转码超快" - :type=="pcm"?"#0b1'>pcm转码超快" + +(enc.fast?"#0b1'>"+type+"转码超快" :Recorder.prototype[type+"_start"]?"#0b1'>支持边录边转码(Worker)" :"red'>仅支持标准UI线程转码") +","+enc.testmsg]; diff --git a/assets/demo-vue/README.md b/assets/demo-vue/README.md index ae9b8aa..7ebb633 100644 --- a/assets/demo-vue/README.md +++ b/assets/demo-vue/README.md @@ -1,5 +1,7 @@ # vue+webpack测试 +支持`Vue2`、`Vue3`,自己编写代码可按照仓库首页的`README.md`正常`import Recorder from 'recorder-core'`使用就行,另外专门写了一篇文章《[vue3实现H5网页录音并上传(mp3、wav)兼容Android、iOS和PC端](https://www.cnblogs.com/xiangyuecn/p/17472952.html)》方便参考。 + - [Recorder H5在线测试](https://xiangyuecn.gitee.io/recorder/assets/demo-vue),主要文件为 [component/recorder.vue](https://github.com/xiangyuecn/Recorder/blob/master/assets/demo-vue/component/recorder.vue),支持PC、Android、IOS 14.3+。 - [RecordApp 在线测试](https://jiebian.life/web/h5/github/recordapp.aspx?path=/assets/demo-vue/recordapp.html),主要文件为 [component/recordapp.vue](https://github.com/xiangyuecn/Recorder/blob/master/assets/demo-vue/component/recordapp.vue) [即将废弃] ,RecordApp除了Recorder支持的外,支持Hybrid App,低版本IOS上支持微信网页和小程序web-view。 diff --git a/assets/demo-vue/index.html b/assets/demo-vue/index.html index 605e548..9e5b9e9 100644 --- a/assets/demo-vue/index.html +++ b/assets/demo-vue/index.html @@ -48,6 +48,7 @@ logMeta('UA',navigator.userAgent); logMeta('URL',location.href.replace(/#.*/g,"")); logMeta('Vue',vue_vue.version); +mainRef.reclog('支持Vue2、Vue3,自己编写代码可按照仓库首页的README.md正常`import Recorder from \'recorder-core\'`使用就行,另外专门写了一篇文章《vue3实现H5网页录音并上传(mp3、wav)兼容Android、iOS和PC端》方便参考'); mainRef.reclog('当前浏览器支持录音':'red">不支持录音')+''); mainRef.reclog("点击打开录音,然后再点击录制开始录音",2); diff --git a/assets/npm-home/hash-history.txt b/assets/npm-home/hash-history.txt index 78af205..7f86b28 100644 --- a/assets/npm-home/hash-history.txt +++ b/assets/npm-home/hash-history.txt @@ -1,4 +1,8 @@ [ + { + "sha1": "6585fc5475148ebcf7cbb38be77191acb9bcac0e", + "time": "2023/6/17 21:06:29" + }, { "sha1": "4fa1d9dd106f8d959a505168088d935f98fc620d", "time": "2023/6/10 22:27:40" @@ -14,9 +18,5 @@ { "sha1": "820a2fe65d47fd2b2a88d7bfd5456266dbb39005", "time": "2022-8-7 18:51:17" - }, - { - "sha1": "40d86b5656875a4f856d652bc3d4839464d8fe2d", - "time": "2022-6-28 09:44:33" } ] \ No newline at end of file diff --git a/assets/runtime-codes/dsp.lib.filter.iir.js b/assets/runtime-codes/dsp.lib.filter.iir.js index b4a1a58..ecf124a 100644 --- a/assets/runtime-codes/dsp.lib.filter.iir.js +++ b/assets/runtime-codes/dsp.lib.filter.iir.js @@ -247,10 +247,14 @@ var test=function(fn,useFS){ } Runtime.Log("开始转换"+fn+" "+fnName+(useFS?".FS":"")+":"+JSON.stringify({lowPass:lowPassHz,highPass:highPassHz,sampleRate:newSampleRate,srcSampleRate:srcSampleRate}),"#aaa"); + + if(!Recorder.__IIRFilterBak)Recorder.__IIRFilterBak=Recorder.IIRFilter; + Recorder.IIRFilter=function(){return function(v){return v}};//禁用默认的滤波 var rec=Recorder({ type:"wav",bitRate:16,sampleRate:newSampleRate||srcSampleRate }).mock(pcm,srcSampleRate); rec.stop(function(blob,duration){ + Recorder.IIRFilter=Recorder.__IIRFilterBak; Runtime.LogAudio(blob,duration,rec,"已转换"+fn); }); }; \ No newline at end of file diff --git a/assets/runtime-codes/test.g7xx.engine.js b/assets/runtime-codes/test.g7xx.engine.js new file mode 100644 index 0000000..f1c5b63 --- /dev/null +++ b/assets/runtime-codes/test.g7xx.engine.js @@ -0,0 +1,338 @@ +/****************** +《【测试】G711、G72X编码和解码播放》 +作者:高坚果 +时间:2023-05-04 19:46 + +G711标准(1988): https://www.itu.int/rec/T-REC-G.711/en +G72X: https://www.itu.int/rec/T-REC-G.721/en + https://www.itu.int/rec/T-REC-G.723/en + https://www.itu.int/rec/T-REC-G.726/en + https://www.itu.int/rec/T-REC-G.729/en + +FFmpeg转码: +[ wav->pcma] ffmpeg -i test.wav -acodec pcm_alaw -f alaw -ac 1 -ar 8000 test.pcma +[ wav->pcmu] ffmpeg -i test.wav -acodec pcm_mulaw -f mulaw -ac 1 -ar 8000 test.pcmu +[ wav->pcm ] ffmpeg -i test.wav -f s16le -ac 1 -ar 16000 test.pcm +[ pcm->pcma] ffmpeg -f s16le -ac 1 -ar 16000 -i test.pcm -acodec pcm_alaw -f alaw -ac 1 -ar 8000 test.pcm.pcma + +FFmpeg播放: + ffplay -f alaw -ac 1 -ar 8000 test.pcma + ffplay -f mulaw -ac 1 -ar 8000 test.pcmu + ffplay -f s16le -ac 1 -ar 16000 test.pcm + +FFmpeg下载,解压得到 ffmpeg ffplay + https://ffmpeg.org/download.html https://www.gyan.dev/ffmpeg/builds/ +FFmpeg命令行参数: + ffmpeg [infile options] -i infile [outfile options] outfile +******************/ + +var regEngine=function(key,enc,dec){ + Recorder.prototype[key]=function(res,True,False){ + var This=this,set=This.set,srcSampleRate=set.sampleRate,sampleRate=8000; + set.bitRate=16; + if(srcSampleRate!=sampleRate){ + set.sampleRate=sampleRate; + res=Recorder.SampleData([res],srcSampleRate,sampleRate).data; + } + var bytes=enc(res); + True(new Blob([bytes.buffer],{type:"audio/"+key})); + }; + Recorder[key+"2wav"]=function(blob,True,False){ + var reader=new FileReader(); + reader.onloadend=function(){ + var bytes=new Uint8Array(reader.result); + try{ var pcm=dec(bytes) } + catch(e){ return False(e.message) } + Recorder({ + type:"wav",sampleRate:8000,bitRate:16 + }).mock(pcm,8000).stop(function(wavBlob,duration){ + True(wavBlob,duration); + },False); + }; + reader.readAsArrayBuffer(blob); + }; +}; + +//https://github.com/dystopiancode/pcm-g711/blob/master/pcm-g711/g711.c +regEngine("g711a_1",function(pcm){ + var buffer=new Int8Array(pcm.length); + for(var i=0;i ALAW_MAX) { number = ALAW_MAX; } + for (; ((number & mask) != mask && position >= 5); mask >>= 1, position--); + lsb = (number >> ((position == 4) ? (1) : (position - 4))) & 0x0f; + buffer[i] = (sign | ((position - 4) << 4) | lsb) ^ 0x55; + } + return new Uint8Array(buffer.buffer); +},function(bytes){ return g711aDec(bytes) }); + +//https://blog.csdn.net/u012758497/article/details/113197704 +regEngine("g711a_2",function(pcm){ + var aLawCompressTable=[1, 1, 2, 2, 3, 3, 3, + 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7]; + var cClip = 32635; + + var buffer=new Uint8Array(pcm.length); + for(var i=0;i> 8) & 0x80; + if (!(sign == 0x80)) { + sample = -sample; + } + if (sample > cClip) { + sample = cClip; + } + if (sample >= 256) { + var exponent = aLawCompressTable[(sample >> 8) & 0x7F]; + var mantissa = (sample >> (exponent + 3)) & 0x0F; + s = (exponent << 4) | mantissa; + } else { + s = sample >> 4; + } + s ^= (sign ^ 0x55); + buffer[i] = s; + } + return buffer; +},function(bytes){ return g711aDec(bytes) }); + +//https://github.com/twstx1/codec-for-audio-in-G72X-G711-G723-G726-G729/blob/master/G711_G721_G723/g711.c +var Tab=[1,2,3,3,4,4,4,4,5,5,5,5,5,5,5,5,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7]; +regEngine("g711a",function(pcm){ + /*var seg_end=[0xFF, 0x1FF, 0x3FF, 0x7FF, 0xFFF, 0x1FFF, 0x3FFF, 0x7FFF]; + //生成Tab + var tab=[]; + for(var i=0;i<=0x7fff;i++){ + var seg=8; + for (var i2 = 0; i2 < 8; i2++) { if(i<=seg_end[i2]) { seg=i2; break; } } + var v=i>>8&0x7F; + if(tab[v]==null) tab[v]=seg; + else if(tab[v]!=seg) throw new Error("") + } + console.log(tab,JSON.stringify(tab)); + tab=tab.map(a=>a+1).filter(a=>a!=8); + console.log(tab,JSON.stringify(tab)); + */ + + var buffer=new Uint8Array(pcm.length); + for(var i=0;i= 0) { + mask = 0xD5; /* sign (7th) bit = 1 */ + } else { + mask = 0x55; /* sign bit = 0 */ + pcm_val = -pcm_val - 1; + } + + /* Convert the scaled magnitude to segment number. */ + //for (var i2 = 0; i2 < 8; i2++) { if(pcm_val<=seg_end[i2]) { seg=i2; break; } } + seg = (Tab[pcm_val>>8&0x7F]||8)-1; + + + /* Combine the sign, segment, and quantization bits. */ + aval = seg << 4; + if (seg < 2) + aval |= (pcm_val >> 4) & 15; + else + aval |= (pcm_val >> (seg + 3)) & 15; + buffer[i] = (aval ^ mask); + } + return buffer; +},function(bytes){ return g711aDec(bytes) }); +var g711aDec=function(bytes){ + var buffer=new Int16Array(bytes.length); + for(var i=0;i> 4; + switch (seg) { + case 0: + t += 8; break; + case 1: + t += 0x108; break; + default: + t += 0x108; + t <<= seg - 1; + } + buffer[i] = ((a_val & 0x80) ? t : -t); + } + return buffer; +}; + +regEngine("g711u",function(pcm){ + var buffer=new Uint8Array(pcm.length); + for(var i=0;i>8&0x7F]||8)-1; + + uval = (seg << 4) | ((pcm_val >> (seg + 3)) & 0xF); + buffer[i] = (uval ^ mask); + } + return buffer; +},function(bytes){ + var buffer=new Int16Array(bytes.length); + for(var i=0;i> 4; + + buffer[i] = ((u_val & 0x80) ? (0x84 - t) : (t - 0x84)); + } + return buffer; +}); + + + + + + +//=====测试代码================== +//加载录音框架 +Runtime.Import([ + {url:RootFolder+"/src/recorder-core.js",check:function(){return !window.Recorder}} + ,{url:RootFolder+"/src/engine/wav.js",check:function(){return !Recorder.prototype.wav}} + + ,{url:RootFolder+"/assets/runtime-codes/fragment.decode.wav.js",check:function(){return !window.DemoFragment||!DemoFragment.DecodeWav}} +]); + +//显示控制按钮 +Runtime.Ctrls([ + {html:'
'} + ,{html:'
'} + ,{name:"开始录音",click:"recStart"} + ,{name:"结束录音",click:"recStop"} + + ,{choiceFile:{cls:"choiceFileDec", + multiple:true + ,name:"g7xx",title:"解码播放G7XX" + ,mime:"*/*" + ,process:function(fileName,arrayBuffer,filesCount,fileIdx,endCall){ + var type=(/\.([^\.]+)/.exec(fileName)[1]||"error").toLowerCase(); + if(type=="pcma") type="g711a"; + if(type=="pcmu") type="g711u"; + var blob=new Blob([arrayBuffer]); + var duration=blob.size/8000*1000; + Runtime.LogAudio(blob,duration,{set:{type:type,bitRate:16,sampleRate:8000}},"播放文件"); + endCall(); + } + }} + ,{choiceFile:{ cls:"choiceFileEnc",keepOther:true, + multiple:false + ,name:"wav",title:"转码成G7XX" + ,mime:"audio/wav" + ,process:function(fileName,arrayBuffer,filesCount,fileIdx,endCall){ + try{ + var data=DemoFragment.DecodeWav(new Uint8Array(arrayBuffer)); + }catch(e){ + Runtime.Log(fileName+"解码失败:"+e.message,1); + return endCall(); + } + test(data.pcm,data.sampleRate); + Runtime.Log("文件测试完成"); + endCall(); + } + }} +]); +$(".testChoiceFileDec").append($(".choiceFileDec")); +$(".testChoiceFileEnc").append($(".choiceFileEnc")); + + +//调用录音 +var rec; +function recStart(){ + rec=Recorder({ + type:"wav" + ,sampleRate:8000 + ,bitRate:16 + ,onProcess:function(buffers,powerLevel,bufferDuration,bufferSampleRate){ + Runtime.Process.apply(null,arguments); + } + }); + var t=setTimeout(function(){ + Runtime.Log("无法录音:权限请求被忽略(超时假装手动点击了确认对话框)",1); + },8000); + + rec.open(function(){//打开麦克风授权获得相关资源 + clearTimeout(t); + rec.start();//开始录音 + },function(msg,isUserNotAllow){//用户拒绝未授权或不支持 + clearTimeout(t); + Runtime.Log((isUserNotAllow?"UserNotAllow,":"")+"无法录音:"+msg, 1); + }); +}; +function recStop(){ + if(!rec){ + Runtime.Log("未开始录音",1); + return; + } + rec.stop(function(blob,duration){ + Runtime.LogAudio(blob,duration,rec); + + test(Recorder.SampleData(rec.buffers,rec.srcSampleRate,rec.srcSampleRate).data,rec.srcSampleRate); + },function(msg){ + Runtime.Log("录音失败:"+msg, 1); + },true); +}; + + + +var test=function(pcm,sampleRate){ + console.log(new Uint8Array(pcm.buffer)); + var run=function(type){ + var rec=Recorder({type:type}); + rec.mock(pcm,sampleRate); + rec.stop(function(blob,duration){ + Runtime.LogAudio(blob,duration,rec,type); + }); + } + var rec=Recorder({type:"wav",sampleRate:16000}); + rec.mock(pcm,sampleRate); + rec.stop(function(blob,duration){ + Runtime.LogAudio(blob,duration,rec,"wav16k"); + + rec=Recorder({type:"wav",sampleRate:8000,bitRate:8}); + rec.mock(pcm,sampleRate); + rec.stop(function(blob,duration){ + Runtime.LogAudio(blob,duration,rec,"wav8k8"); + + rec=Recorder({type:"wav",sampleRate:8000}); + rec.mock(pcm,sampleRate); + rec.stop(function(blob,duration){ + Runtime.LogAudio(blob,duration,rec,"wav8k"); + + //run("g711a_1"); + //run("g711a_2"); + run("g711a"); + run("g711u"); + }); + }); + }); +}; +Runtime.Log("测试方法:
① 拖入或录制wav文件转码成g7xx,下载后用ffmpeg播放试听
② ffmpeg生成g7xx文件拖入页面播放进行解码测试"); +//test(new Int16Array([0x1000,0x6000,0x60,0x7000,0x3000,-0x60,-0x7000,-0x3000].concat(new Array(992))),8000); diff --git "a/assets/\345\267\245\345\205\267-\344\273\243\347\240\201\350\277\220\350\241\214\345\222\214\351\235\231\346\200\201\345\210\206\345\217\221Runtime.html" "b/assets/\345\267\245\345\205\267-\344\273\243\347\240\201\350\277\220\350\241\214\345\222\214\351\235\231\346\200\201\345\210\206\345\217\221Runtime.html" index deacbf7..1f3f566 100644 --- "a/assets/\345\267\245\345\205\267-\344\273\243\347\240\201\350\277\220\350\241\214\345\222\214\351\235\231\346\200\201\345\210\206\345\217\221Runtime.html" +++ "b/assets/\345\267\245\345\205\267-\344\273\243\347\240\201\350\277\220\350\241\214\345\222\214\351\235\231\346\200\201\345\210\206\345\217\221Runtime.html" @@ -449,7 +449,7 @@ end(blob); logmsg("已转码成wav播放"); },function(msg){ - logmsg("转码成wav失败:"+msg); + logmsg('转码成wav失败:'+msg+''); }); }else{ end(o.blob); @@ -497,24 +497,29 @@ ,title:"转换" ,mime:"audio/*" ,process:NOOP //fn(fileName,arrayBuffer,filesCount,fileIdx,endCall) + ,keepOther:false + ,cls:"RuntimeChoiceFile_"+(++Rnd) },set); - $(".RuntimeChoiceFileBox").remove(); + var cls=set.cls; + if(!set.keepOther){ + $(".RuntimeChoiceFileBox").remove(); + } Runtime.Log('\ -
\ -
\ +
\ +
\ 拖拽'+(set.multiple?"多":"一")+'个'+set.name+'文件到这里 / 点此选择,并'+set.title+'\
\ \
'); - $(".RuntimeChoiceDropFile").bind("dragover",function(e){ + $("."+cls+" .RuntimeChoiceDropFile").bind("dragover",function(e){ e.preventDefault(); }).bind("drop",function(e){ e.preventDefault(); readChoiceFile(e.originalEvent.dataTransfer.files); }); - $(".RuntimeChoiceFile").bind("change",function(e){ + $("."+cls+" .RuntimeChoiceFile").bind("change",function(e){ readChoiceFile(e.target.files); }); function readChoiceFile(files){ @@ -1130,6 +1135,7 @@ ,{n:"【Demo库】【信号处理】IIR低通、高通滤波",k:"dsp.lib.filter.iir"} ,{n:"【测试】【信号处理】FFT频域分析ECharts频谱曲线图",k:"dsp.test.fft.analysis"} ,{n:"【测试】WebM格式解析并提取音频",k:"test.webm.extract_audio"} +,{n:"【测试】G711、G72X编码和解码播放",k:"test.g7xx.engine"} ,{n:"【测试】音频可视化相关插件测试",k:"test.extensions.visualization"} ]; diff --git "a/assets/\345\267\245\345\205\267-\350\243\270PCM\350\275\254WAV\346\222\255\346\224\276\346\265\213\350\257\225.html" "b/assets/\345\267\245\345\205\267-\350\243\270PCM\350\275\254WAV\346\222\255\346\224\276\346\265\213\350\257\225.html" index e2c9299..bed8e05 100644 --- "a/assets/\345\267\245\345\205\267-\350\243\270PCM\350\275\254WAV\346\222\255\346\224\276\346\265\213\350\257\225.html" +++ "b/assets/\345\267\245\345\205\267-\350\243\270PCM\350\275\254WAV\346\222\255\346\224\276\346\265\213\350\257\225.html" @@ -7,6 +7,7 @@ 裸(RAW、WAV)PCM转WAV播放测试和转码 +