最近需要做一个实时录音然后根据音频流实时反馈出调用静音分析(VAD)以及语音识别(ASR)接口的功能。于是研究起H5有关这方面的支持。
H5的Web Audio API
首先需要弄清一点,Web Audio API
和H5的<audio>
完全不是一个体量级的东西,<audio>
可以很方便地让你将音频文件丢进去就自带各种花式功能。但是如果直接用Web Auido API
进行操作,你甚至可以无中生有地创造声音。
这里粗糙地罗列一下它能做什么事情(尽我所能查到的资料):
<audio>
能做什么:
简单的音频播放器;
「单线程」的音频;
web Audio API
:
- 对简单或复杂的声音进行混合;
- 精确地控制声音的密度和节奏;
- 内置淡入、淡出,颗粒噪点、音调控制等效果;
- 灵活的处理在音频流的声道,使它们成为拆分和合并;
- 处理从
<audio>
音频或<video>
视频的媒体元素和音频源。 - 使用
MediaStream的getUserMedia()
方法实时处理现场输入的音频,例如变声; - 立体音效,支持多种3D游戏和沉浸式环境。
- 利用卷积引擎,创造各种线性音效,例如小/大房间、大教堂、音乐厅、洞穴、隧道、走廊、森林等。尤其适用于创建高质量的房间效果。(就是音乐播放器的那种特效~)
- 高效实时的时域和频的分析,配合CSS3或Canvas或WebGL可以做到音乐的可视化支持。
- 音频波形算法控制,也就是只要你研究地足够透彻,你把AU搬到web上实现也就阔以的。
然而,这么多高深地功能,很多前端开发者其实都没有这样地需求去接触。罗列在这里,是为了让自己接到类似需求的时候能准确判断对此类需求能做到什么程度。
简单生成点声音
网页一般是无声的,但是当你尝试去给你的点击产生一个声音,对于特殊场景下会让客户耳目一新。这里例子主要参考自张鑫旭–利用HTML5 Web Audio API给网页交互增加声音给到的例子。我们逐行来分析一下代码来看看如何实现这种效果。
|
|
oscillator
需要了解上面的代码我们需要对音频有一些基础的认识。首先,声音的本质其实就是震动,而震动又务必牵扯到波形,不同的波形会发出不同的声音。然后相同的波形下还会有不一样的震动频率,最终会表现为音调的高低。因此当我们需要生成一个声音的时候,就需要为它设置波形以及对应的音调,所以你可以这么理解oscillator
就是一个创造音调的玩意。
那么给一个音调的创造过程就如下:设置波形–>设置频率
波形
波形主要内置了4种波形,对应发出不同的声音。主要有sine
(正弦波)、square
(方波)、triangle
(三角波)以及sawtooth
锯齿波。
当然如果有需要还可以使用setPeriodicWave
自定义波形。
频率
频率这玩意很好理解。就是我们生活中接触地“do、re、mi、fa、sol、la、si”.数值越小,越低沉;数值越大,越清脆。
connect
其实是h5audio一个十分重要的概念。具体我把它简单理解为中间件的一个概念。例如这个例子,音调产生后经过音量处理的中间件然后再将这些声音结点输出到扬声器上。(在其他文章有很介绍得十分详尽的,在这里就不太想展开,最下面的外链可以找到)
剩下的就是淡入淡出的设置以及播放声音的内容。这些部分在张大大的博客中有详细地阐述,该块内容也是参考着它的博文进行二次翻译记录自己一些理解(很粗浅)然后总结罢了。
玩点录音
获取到原始的pcm数据
自己创造声音有点过于高大上,只播声音又显得有点无趣。那么干脆来玩一下录音好了。开启录音很简单,只需要这么简单的一行代码(当然不考虑兼容性咯)
|
|
可以试一下在浏览器控制台输入这段代码, 你就会看到网站想要调用你的摄像头以及麦克风的请求。点击允许后,你的摄像头就会亮灯,开启录音和录屏的状态。
navigator.mediaDevices.getUserMedia(videoObj, (stream) => {}, errBack)
你应该马上就会问,那么录完的音跑到哪里去了。我们从这个调用这个方法后,看到中间其实是有一个回调函数,让我们拿到麦克风和摄像头产生的数据流的。这时候我们可以调用AudioContext
的接口使得音频PCM数据在到达目的地前通过不同的处理节点(增益、压缩等),所以我们需要从这里来入手。
|
|
做到这一步我们已经能拿到录音数据了,而且还是按照我们预想的样子去得到已经分好片的buffer数据。那么我们终于可以开始愉快地处理我们的录音流了。
补充一下上面代码中注释的两段用来控制录音音量大小以及将录音声音实时反馈到扬声器的两段方法函数。原理相一致,也就是设置一个处理数据的中间件,在录音设备最终走到扬声器(目标地)前进行二进制的控制
|
|
注意在recorderProcess
里面调用了一个getChannelData
的方法,可以传入整型,取到对应声道的数据,进行分别处理。由于我们是单声道录制,所以只需要拿到左声道的数据流即可。
如果这时候你不需要对音频的输出再进行控制,已经可以将这段二进制数据直接用websoket
传输到后台去了。
数据处理与转换
假如真不凑巧,后台大佬要的数据不是你这玩意的样子,大佬们对音频质量有要求:只接受一个采样率是8khz、位深16的wav文件。很好,那么提取关键字,我们需要先确认我们这段pcm数据是否8khz以及位深16.最后再把这些二进制组合起来转成wav格式。看起来很复杂,没关系,我们一步一步来。
采样率(sampleRate)和位深度(bitDepth)
首先,采样率(sampleRate)是什么呢,百度一下。
音频采样率是指录音设备在一秒钟内对声音信号的采样次数,采样频率越高声音的还原就越真实越自然。在当今的主流采集卡上,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级,22.05KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则更加精确一些。
那么接下来这段代码就可以让你获取到你麦克风采样率是多少。
很好,这段输出代表你的录音设备采样率高到44100HZ,那么根据需求,你就需要将自己的音频采样率降低下来了。然而不幸的是,浏览器并不允许去修改录音时的采样率,而且不同电脑设备的表现还不一样。这意味着,你需要在中间node拿到的二进制再进行一次处理。那么怎么去降低自己的采样率呢。根据上面百度的资料,你很容易就能发现,采样率的高低其实只是在一秒内的音频它的数据点有多少个的问题,我们需要把原来一秒内有44100个点的数据流减少成8000个点的数据流。很简单嘛,不是么,不过这个有个小点是需要注意的。
Downsample PCM audio from 44100 to 8000参考这里的一个回复:
Lets take a simple case of downsampling by a factor of 2. (e.g. 44100->22050). A naive approach would be to just throw away every other sample. But imagine for a second that in the original 44.1kHz file there was a single sine wave present at 20khz. It is well within nyquist (fs/2=22050) for that sample rate. After you throw every other sample away it is still going to be there at 10kHz but now it will be above nyquist (fs/2=11025) and it will alias into your output signal. The final result is that you will have a big fat sine wave sitting at 8975 Hz!
In order to avoid this aliasing during downsampling you need to first design a lowpass filter with a cutoff selected according to your decimation ratio. For the example above you would cutoff everything above 11025 first and then decimate.
这里的大概意思就是单纯地抽点是不行的。当然里面内在逻辑还涉及到频率波长什么的,我自然就不太清楚了。有兴趣的朋友可以详细了解一下原因。所幸这里还提供了代码的实现方式。
PS:补充一个别人别供的实现库,具体没看纯属作为参考resampler.js
|
|
那么采样率的问题就解决了。剩下还有两个问题。将音频流转为16位深,这里就比较简单了。只要确保你生成的位数足够就行。比如,8位深的音频只需要生成一个Uint8Array
,16位深就要生成2个的长度。new Unit8Array(bitDepth / 8)
pcm数据转wav
好不容易终于走到最后一步了。怎样把pcm
数据转为wav
数据。本来以为将是一个极其棘手的问题,但是所幸。pcm
->wav
的方法非常简单,将wav
文件以二进制的方式打开后,去除前44位的字节的头文件信息就是一段pcm
数据了。那么我们只需要把录音过程中的所有内容都收集起来,然后插入头文件信息即可。这一步相对于前面来说简直简单太多了。
但是我们还有一个问题要解决。“javascript如何操作二进制数据”。所以接下来就要介绍这几个玩意了。
Bolb、ArrayBuffer、Unit8Array、DataView
经过漫长地查询,看源码看API查资料,我终于集齐了有关这次操作的所有对象,可以召唤神龙了!
这个几个对象真的是老衲学了这么久都没接触过几个,有些甚至听都没听过。所以一个一个来,而且我也只能提供一些我初略的见解。(欢迎有大佬给我订正,想要具体了解还是更适合单独去搜索)
ArrayBuffer
是个啥玩意呢。ArrayBuffer
又称类型化数组。类型化数组,我记得在一篇详解数组的文章有看到,但是由于用处不多后面就给忘了。大致的意思就是js的数组对象,其实并不是像其他面向对象语言的实现方式一样,内存不连续而且类型不可控严重影响性能。(但是各大浏览器引擎都有做优化,所以实际编码不需要考虑)。而ArrayBuffer
则是专门放0和1组成的二进制数据,所以当你在捕获到pcm数据的时候,将它打印到控制台上会看到这是个ArrayBuffer
类型的数组,就是因为这段是二进制数据。
而ArrayBuffer
对象并没有提供任何读写内存的方法,而是允许在其上方建立“视图”,从而插入与读取内存中的数据。那么视图又是啥呢??
视图类型 | 数据类型 | 占用位数 | 占用字节 | 有无符号 |
---|---|---|---|---|
Int8Array | 整数 | 8 | 1 | 有 |
Uint8Array | 整数 | 8 | 1 | 无 |
Uint8ClampedArray | 整数 | 8 | 1 | 无 |
Int16Array | 整数 | 16 | 2 | 有 |
Uint16Array | 整数 | 16 | 2 | 无 |
Int32Array | 整数 | 32 | 4 | 有 |
Uint32Array | 整数 | 32 | 4 | 无 |
Float32Array | 整数 | 32 | 4 | \ |
Float64Array | 浮点数 | 64 | 8 | \ |
这对于一个计算机基础极差的大兄弟来说简直是噩梦。这么多玩意,我得怎么搞。又怎么选择,啊咧要崩溃了。但是要操作二进制呀,求助源码库是一个最简单的做法。在源码里面就发现这个玩意了。
DataView
视图。为了解决各种硬件设备、数据传输等对默认字节序的设定不一而导致解码时候会发生的混乱问题,javascript
提供了DataView
类型的视图来让开发者在对内存进行读写时手动设定字节序的类型。于是.wav
的文件头就要这么写。具体想知道怎么写还是得百度出来.wav
的文件头信息字节具体如何分配
|
|
上面的内容就是头信息的写法了。最后将数据以unit8Array
的格式写入一个wav二进制数据就有了。我们还需要的就是将它转成文件对象。
Blob
你可能没听过,但是File
你肯定听过,因为经常需要form表单传文件嘛。那么你这么理解,Blob
是一种JavaScript
的对象类型。HTML5
的文件操作对象,file
对象就是Blob
的一个分支或说一个子集。
也就是为啥File
对象能进行文件分割上传,就是利用了Blob
操作二进制数据的方法。
|
|
验证效果
上面的代码都是二进制操作,是不是特别没信息。那就验证一下吧!把生成的blob
对象,这样操作:const url = URL.createObjectURL(blob)
然后把它丢到<a>
标签里面来下载这个文件。命令行工具:
|
|
完全符合效果。nice,剩下的问题就是回到实时处理pcm数据的问题了。
传输arrayBuffer
文件我们会传,但是二进制buffer数组要怎么传递呢?只有当你使用blob
生成后的对象才能作为文件对象使用。但是当我们还是pcm数据段的时候怎么来作为一个文件传递过去呢。
这里只稍微列举一下自己的尝试,不一定是个正确的使用方法。这里使用的是websocket
|
|
做点小优化
Web Worker
了解一下,由于涉及到文件的转码操作。所以很耗费性能,这时候是时候掏出web Worker
来深度优化这玩意。
有关这个的内容,其实看起来很高深但是很简单,worker
就相当于一个处理源数据的方法,然后交互方式是用事件监听。
这里仅仅记录几个坑,就不详细介绍:
- web worker必须起服务来实现。单纯html文件时不能打开worker的。worker必须要传一个
.js
文件路径,且保证同源策略。 - 不要向worker去传递函数,否则会错误。而且浏览器还不暴露这个错误。排查了好久的说
- worker必须传入一个js的路径。在vue下,只能把它丢在static文件去引用了喵。
这里还有些小问题没解决
- 如何实时地绘制出录音的音量。首先音量可以取得到的buffer里面的大小作为分贝参考。但是具体实现方式还不太懂,或者说没有简便的实现方式吧。
- 前端收到一个buffer的时候想要实时播放如何做到,现在做的方式其实是最后才嵌回去。做了一个小实验,没成功。(直接把录音转化好的内容转回ArrayBuffer但是失败了)。感觉如果有后端来传递数据应该可以实现。
- 12345678audioContext.decodeAudioData(play_queue[index_play], function(buffer) {//解码成pcm流var audioBufferSouceNode = audioContext.createBufferSource();audioBufferSouceNode.buffer = buffer;audioBufferSouceNode.connect(audioContext.destination);audioBufferSouceNode.start(0);}, function(e) {console.log("failed to decode the file");});
如何实时地计算到现在音频的录制实现。因为是数据流没办法转成音频对象导致没能拿到这些方法来获取。现在比较粗暴的方法就是通过字节长度 采样率 位数来计算 但是感觉计算量比较大 不属于一个好的实现方式?
- 改变音调和声音位置的api没能成功调用起来。
|
|
|
|
node端切割音频文件
这里补充一点小小知识点。node端并没有特别好的处理音频的库。查询了很久之后发现一个调用机器环境ffmpeg
来辅助处理的库。用起来起码是没问题的,而且非常全面。(毕竟接触音频多的人应该都懂这个玩意)这里粗糙mark一下自己之前用到的api。
|
|
最后,感谢付总、涂老师耐心地和我讲解有关音频的种种问题。感谢前端组各位大佬在我不懂的内容即使自己也没有过多接触但是还是耐心和我探讨问题。
参考文章:这里列出的文章都是我查找资料时看到的不错的文章。和上面的内容不一定强烈相关,建议对音频感兴趣的朋友完全可以自己看看
Getting Started with Web Audio API
Using Recorder.js to capture WAV audio in HTML5 and upload it to your server or download locally
Tutorial: HTML Audio Capture streaming to Node.js (no browser extensions)
[前端教程]HTML5制作好玩的麦克风音量检测器(Web Audio API)
Downsample PCM audio from 44100 to 8000
DataView
Typed Arrays: Binary Data in the Browser
Tech Tip: Sample Rate and Bit Depth—An Introduction to Sampling
How to convert ArrayBuffer to and from String
用html5-audio-api开发游戏的3d音效和混音
理解DOMString、Document、FormData、Blob、File、ArrayBuffer数据类型
深入浅出 Web Audio Api
更改AudioContext的采样率(getUserMedia)——这里面有讨论到AudioContext
的一些更改和进展吧,也算一点扩展知识