Skip to content

AudioContext,网页录音,音频下载 #2

Description

@pkjy

一张图概括录音+本地播放

前端实现录音有两种方式,一种是使用MediaRecorder,另一种是使用WebRTC的getUserMedia结合AudioContext,MediaRecorder出现得比较早,只不过Safari/Edge等浏览器一直没有实现,所以兼容性不是很好,而WebRTC已经得到了所有主流浏览器的支持,如Safari 11起就支持了。所以我们用WebRTC的方式进行录制。

利用AudioContext播放声音的使用,我已经在《Chrome 66禁止声音自动播放之后》做过介绍,本篇我们会继续用到AudioContext的API.

为实现录音功能,我们先从播放本地文件音乐说起,因为有些API是通用的。

播放本地音频文件实现

播放音频可以使用audio标签,也可以使用AudioContext,audio标签需要一个url,它可以是一个远程的http协议的url,也可以是一个本地的blob协议的url,怎么创建一个本地的url呢?

使用以下html做为说明:
image

提供一个file input上传控件让用户选择本地的文件和一个audio标签准备来播放。当用户选择文件后会触发onchange事件,在onchange回调里面就可以拿到文件的内容,如下代码所示:
image

这里使用一个FileReader读取文件,读取为ArrayBuffer即原始的二进制内容,把它打印如下所示:
image

可以用这个ArrayBuffer实例化一个Uint8Array就能读取它里面的内容,Uint8Array数组里面的每个元素都是一个无符号整型8位数字,即0 ~ 255,相当于每1个字节的0101内容就读取为一个整数。

这个arrayBuffer可以转成一个blob,然后用这个blob生成一个url,如下代码所示:
image

主要利用URL.createObjectURL这个API生成一个blob的url,这个url打印出来是这样的:

blob:null/c2df9f4d-a19d-4016-9fb6-b4899bac630d

然后丢给audio标签就能播放了,作用相当于一个远程的http的url.

在使用ArrayBuffer生成blob对象的时候可以指定文件类型或者叫mime类型,如下代码所示:
image
这个mime可以通过file input的files[0].type得到,而files[0]是一个File实例,File有mime类型,而Blob也有,因为File是继承于Blob的,两者是同根的。所以在上面实现代码里面其实不需要读取为ArrayBuffer然后再封装成一个Blob,直接使用File就行了,如下代码所示:
image

而使用AudioContext需要拿到文件的内容,然后手动进行音频解码才能播放。

AudioContext的模型

使用AudioContext怎么播放声音呢,我们来分析一下它的模型,如下图所示:
image

我们拿到一个ArrayBuffer之后,使用AudioContext的decodeAudioData进行解码,生成一个AudioBuffer实例,把它做为AudioBufferSourceNode对象的buffer属性,这个Node继承于AudioNode,它还有connect和start两个方法,start是开始播放,而在开始播放之前,需要调一下connect,把这个Node连结到audioContext.destination即扬声器设备。代码如下所示:

image

把解码后的audioBuffer打印出来,如下图所示:

image

他有几个对开发人员可见的属性,包括音频时长,声道数量和采样率。从打印的结果可以知道播放的音频是2声道,采样率为44.1k Hz,时长为196.8s。关于声音这些属性的意义可见《从Chrome源码看audio/video流媒体实现一》.

从上面的代码可以看到,利用AudioContext处理声音有一个很重要的枢纽元素AudioNode,上面使用的是AudioBufferSourceNode,它的数据来源于一个解码好的完整的buffer。其它继承于AudioNode的还有GainNode:用于设置音量、BiquadFilterNode:用于滤波、ScriptProcessorNode:提供了一个onaudioprocess的回调让你分析处理音频数据、MediaStreamAudioSourceNode:用于连接麦克风设备,等等。这些结点可以用装饰者模式,一层层connect,如上面代码使用到的bufferSourceNode可以先connect到gainNode,再由gainNode connect到扬声器,就能调整音量了。

如下图示意:
image

这些节点都是使用audioContext的工厂函数创建的,如调createGainNode就可以创建一个gainNode.

说了这么多就是为了录音做准备,录音需要用到ScriptProcessorNode.

录音的实现

上面播放音乐的来源是本地音频文件,而录音的来源是麦克风,为了能够获取调起麦克风并获取数据,需要使用WebRTC的getUserMedia,如下代码所示;
image
在调用getUserMedia的时候指定需要录制音频,如果同时需要录制视频那么再加一个video: true就可以了,也可以指定录制的格式:
image
调用的时候,浏览器会弹一个框,询问用户是否允许使用用麦克风:
image
如果用户点了拒绝,那么会抛异常,在catch里面可以捕获到,而如果一切顺序的话,将会返回一个MediaStream对象:
image
它是音频流的抽象,把这个流用来初始化一个MediaStreamAudioSourceNode对象,然后把这个节点connect连接到一个JavascriptProcessorNode,在它的onaudioprocess里面获取到音频数据,然后保存起来,就得到录音的数据。

如果想直接把录的音直接播放出来的话,那么只要把它connect到扬声器就行了,如下代码所示:
image
但一边录一边播的话,如果没用耳机的话容易产生回音,这里不要播放了。

为了获取录到的音的数据,我们把它connect到一个javascriptProcessorNode,为此先创建一个实例:

image

这里是使用createScriptProcessor创建的对象,需要传三个参数:一个是缓冲区大小,通常设定为4kB,另外两个是输入和输出频道数量,这里设定为双声道。它里面有两个缓冲区,一个是输入inputBuffer,另一个是输出outputBuffer,它们是AudioBuffer实例。可以在onaudioprocess回调里面获取到inputBuffer的数据,处理之后,然后放到outputBuffer,如下图所示:
image
例如我们可以把第1步播放本音频用到的bufferSourceNode连接到jsNode,然后jsNode再连接到扬声器,就能在process回调里面分批处理声音的数据,如降噪。当扬声器把4kB的outputBuffer消费完之后,就会触发process回调。所以process回调是不断触发的。

在录音的例子里,是要把mediaNode连接到这个jsNode,进而拿到录音的数据,把这些数据不断地push到一个数组,直到录音终止了。如下代码所示:

image

我们把inputBuffer打印出来,可以看到每一段大概是0.09s:

image
也就是说每隔0.09秒就会触发一次。接下来的工作就是在process回调里面把录音的数据持续地保存起来,如下代码所示,分别获取到左声道和右声道的数据:
image
打印出来可以看到它是一个Float32Array,即数组里的每个数字都是32位的单精度浮点数,如下图所示:

这里有个问题,录音的数据到底表示的是什么呢,它是采样采来的表示声音的强弱,声波被麦克风转换为不同强度的电流信号,这些数字就代表了信号的强弱。它的取值范围是[-1, 1],表示一个相对比例。

然后不断地push到一个array里面:

image
最后加一个停止录音的按钮,并响应操作:
image
把保存的数据打印出来是这样的:

是一个普通数组里面有很多个Float32Array,接下来它们合成一个单个Float32Array:
image

那为什么一开始不直接就弄成一个单个的,因为这种Array不太方便扩容。一开始不知道数组总的长度,因为不确定要录多长,所以等结束录音的时候再合并一下比较方便。

然后把左右声道的数据合并一下,wav格式存储的时候并不是先放左声道再放右声道的,而是一个左声道数据,一个右声道数据交叉放的,如下代码所示:
image

最后创建一个wav文件,首先写入wav的头部信息,包括设置声道、采样率、位声等,如下代码所示:
image

接下来写入录音数据,我们准备写入16位位深即用16位二进制表示声音的强弱,16位表示的范围是 [-32768, +32767],最大值是32767即0x7FFF,录音数据的取值范围是[-1, 1],表示相对比例,用这个比例乘以最大值就是实际要存储的值。如下代码所示:
image
最后,再用第1点提到的生成一个本地播放的blob url就能够播放刚刚录的音了,如下代码所示:
image
或者是把blob使用FormData进行上传。

整一个录音的实现基本就结束了,代码参考了一个录音库RecordRTC。

小结

回顾一下,整体的流程是这样的:
image
先调用webRTC的getUserMediaStream获取音频流,用这个流初始化一个mediaNode,把它connect连接到一个jsNode,在jsNode的process回调里面不断地获取到录音的数据,停止录音后,把这些数据合并换算成16位的整型数据,并写入wav头部信息生成一个wav音频文件的内存buffer,把这个buffer封装成Blob文件,生成一个url,就能够在本地播放,或者是借助FormData进行上传。这个过程理解了就不是很复杂了。

本篇涉及到了WebRTC和AudioContext的API,重点介绍了AudioContext整体的模型,并且知道了音频数据实际上就是声音强弱的记录,存储的时候通过乘以16位整数的最大值换算成16位位深的表示。同时可利用blob和URL.createObjectURL生成一个本地数据的blob链接。

RecordRTC录音库最后面还使用了webworker进行合并左右声道数据和生成wav文件,可进一步提高效率,避免录音文件太大后面处理的时候卡住了。

参考资料:

待办

音频下载

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions