声波通过气体、液体和固体等介质传播。麦克风内置碳膜受空气中声波的挤压产生振动,根据振动的频率和幅度产生相应的电信号(模拟电信号)。此时的模拟电信号还不能进行传输和存储,需要将其转换为数字信号(二进制bit
位表示),模拟信号的数字化过程称为采样量化。采样量化后的数据称为PCM(Pulse Code Modulation)。如何进行采样量化呢?

PCM参数简介
音频采集(采样量化)对于开发应用的程序员来说非常简单,因为在Android、Linux、Mac OSX和Windows中都提供了对应的API,但是要正确使用这些API需要了解一下PCM的一些概念:量化格式(sample format)、采样率(sample rate)和声道数(channels)。
- 采样率(sample rate)是指每秒进行多少次采样,如44100是指每秒进行44100次采样,采样率越高,声音越细腻(细节越精准)。
- 量化格式(sample format)是指每一个采样处波形的幅值对应的一个数值,如果这个数值用16位的二进制整数来表示,那么波形的量化范围是-32768~32767,使用的位数越高精度也就越高,存储的音频在数模转换时,还原就越准确。
- 声道数(channels),简单点说,对于播放时,如果是单声道就是只有一个喇叭发声,双声道就是两个喇叭发声;对于采集来说,双声道就是有两个麦克风。虽然有些设备支持双声道采集,但是采集的数据跟单声道没有区别,一是声音来源只有一处,二是设备不够专业。对于程序员来说,16次采样量化,单声道就是16次采样,而双声道包括8次左声道和8次右声道的采样,且是交替排列的。
num channels | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
Mono | frame | frame | frame | frame | frame | frame | frame | frame |
Stereo | Lframe | Rframe | Lframe | Rframe | Lframe | Rframe | Lframe | Rframe |
采样得到PCM数据后,需要记下采样率、量化格式和声道数三个参数,不然PCM数据无法正确播放;而且如果要对PCM数据进行编码压缩,这三个参数也是非常重要的。
音频采集
本文主要以Linux的音频采集为例,只要了解了Linux的音频采集的API,对于其他系统来说也是信手拈来(毕竟大同小异)。
麦克风音频采集
在Linux系统中可以使用ALSA(Advanced Linux Sound Architecture)
- 有效支持所有类型的音频接口,包括消费级声卡到多通道音频接口的专业级声卡。
- 完全模块化的音频驱动序。
- SMP和线程安全设计。
- alsa-lib库使使编写程序更加简单,且提供更高级别的功能。
- 支持OSS API,兼容大多数OSS程序。
具体的使用这里不做介绍,因为有更好的参考资料Tutorials for application developers
音频输出捕获
如果采集电脑扬声器输出的音频数据,那么ALSA可能有点力不从心了,还好Audacity给出了一些方法,同时查看FFmpeg的libavdevice源码,也提供了各种音频捕获的方法。经过我的尝试,PulseAudio可以捕获音频输出。
PulseAudio官方示例
在PulseAudio的官方文档中给出了两份示例,其中pacat-simple.c用于PCM数据播放,parec-simple.c用于PCM数据捕获,先来看一下初始化的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/* The sample type to use */
static const pa_sample_spec ss = {
.format = PA_SAMPLE_S16LE,
.rate = 44100,
.channels = 2
};
pa_simple *s = NULL;
int ret = 1;
int error;
/* Create the recording stream */
if (!(s = pa_simple_new(NULL, argv[0], PA_STREAM_RECORD, NULL, "record", &ss, NULL, NULL, &error))) {
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
goto finish;
}
首先设置PCM的参数(前面已经介绍),然后就是创建一个可以循环读取PCM数据的对象pa_simple
,再来看一下函数pa_simple_new
的原型:1
2
3
4
5
6
7
8
9
10
11
12/** Create a new connection to the server. */
pa_simple* pa_simple_new(
const char *server, /**< Server name, or NULL for default */
const char *name, /**< A descriptive name for this client (application name, ...) */
pa_stream_direction_t dir, /**< Open this stream for recording or playback? */
const char *dev, /**< Sink (resp. source) name, or NULL for default */
const char *stream_name, /**< A descriptive name for this stream (application name, song title, ...) */
const pa_sample_spec *ss, /**< The sample type to use */
const pa_channel_map *map, /**< The channel map to use, or NULL for default */
const pa_buffer_attr *attr, /**< Buffering attributes, or NULL for default */
int *error /**< A pointer where the error code is stored when the routine returns NULL. It is OK to pass NULL here. */
);
里面的注释已经很清楚了,但是第四个参数const char *dev
,在前面的示例代码中设为了NULL
,这个是音频源设备的名称,如果是NULL
既为默认设备,我在Linux上验证默认设备为麦克风,那么运行parec-simple
就只能获取麦克风的PCM数据了。如果要捕获音频输出,那么就需要知道音频输出对应的设备名称。
获取音频输出设备
为什么PulseAudio可以捕获音频输出,来看一下PulseAudio的角色:
意思是PulseAudio是一个代理的角色,不管音频是输出还是输入都要经过它,所以它可以捕获任何应用的音频输出(前提是这些应用是使用的PulseAudio的API)。
在介绍怎么获取音频输出设备之前先要明白什么是Sink
和Source
:
- Sink可以理解为音频输出端,如扬声器、耳机等。
- Source为音频输入源,如麦克风。
现在需要做得就是获取Source
设备,获取Source
设备主要调用pa_context_get_source_info_list
方法,具体步骤可以参考pamixer和FFmpeg的pulse_audio_common.c。获取到的设备列表中包括Micphone
等常规音频输入设备外,还有一个
特殊的Source
设备,特殊在它是一个Sink
,可以通过下面的方式判断:1
2
3
4
5
6
7
8
9
10void SourceListCallback(pa_context *, const pa_source_info *info, int eol,
void *userdata) {
if (eol != 0) return;
... ...
if (info != nullptr && info->monitor_of_sink != PA_INVALID_INDEX) {
// Got wanted device
}
}
那么对应的info->name
就是传入方法pa_simple_new
的第四个参数。那么接下来就是循环读取PCM数据了,这里不再赘述。
编码压缩
由于采集到的PCM数据存在冗余信息,可以通过压缩算法将其大小减少到符合当前本地存储和网络传送的限制。网上提供了大量的音频压缩库供使用,这里不进行展开,有时间将在新的文章中进行阐述。
参考资料
[1]. PCM编码示意图