8、音频数据格式:PCM编码、采样率、位深、声道布局
聊到音频回声消除,绕不开一个最基础的问题——音频数据到底长什么样?
我刚开始做音频开发时,拿到一段音频数据,打开十六进制一看,全是字节。当时我就懵了:这堆数字怎么就能变成声音?后来才明白,这些数字背后有一套严格的编码规则。今天咱们就把这套规则掰开揉碎了讲清楚。
8.1 PCM编码:最朴素的音频数字化方式
PCM,全称是脉冲编码调制。说白了,就是把连续的声波信号,每隔一小段时间“咔嚓”拍一张快照,然后把快照的数值记录下来。
这个过程分三步:
- 采样:在时间轴上等间隔取点
- 量化:把采样点的幅度值映射到离散的数值上
- 编码:把量化后的数值用二进制表示
举个例子。你对着麦克风说“你好”,声波是连续变化的。PCM编码后,就变成了一串数字:0x23, 0x45, 0x67, 0x89...。播放时,把这些数字按顺序喂给DAC(数模转换器),就能还原出声音。
核心要点:PCM是未压缩的原始音频数据。回声消除算法直接操作的就是PCM数据。没有编码、没有压缩、没有花里胡哨的包装。
我在项目中遇到过一个问题:某款手机的回声消除效果时好时坏。排查了半天,发现是音频数据在传输过程中被做了重采样,PCM格式变了,但算法没感知到。从那以后,我养成了一个习惯——在任何音频处理链路的入口,先确认PCM格式参数。
8.2 采样率:决定你能听到多高的频率
采样率,就是每秒钟采多少个点。单位是Hz(赫兹)。
为什么采样率这么重要?奈奎斯特采样定理告诉我们:要无失真地还原一个信号,采样率必须至少是信号最高频率的两倍。
人耳能听到的频率范围大约是20Hz~20kHz。所以理论上,40kHz以上的采样率就够用了。但实际工程中,我们常用这几个值:
| 采样率 | 常见场景 | 最高可还原频率 |
|---|---|---|
| 8kHz | 电话语音、对讲机 | 4kHz |
| 16kHz | VoIP通话、语音识别 | 8kHz |
| 48kHz | 高清音频、音乐录制 | 24kHz |
做回声消除时,我个人最常用的是16kHz。为什么?
- 8kHz虽然数据量小,但高频信息丢失太多,回声路径的细节不够
- 48kHz数据量太大,处理延迟高,对实时通话不友好
- 16kHz刚好平衡了质量和性能
我的经验:如果你在做手机端的回声消除,建议先确认一下音频链路的采样率。很多Android设备的麦克风默认是48kHz,但通话链路内部会做重采样到16kHz。如果你直接拿48kHz的数据做处理,可能会和下游不匹配。
8.3 位深:决定声音的动态范围
位深,也叫采样精度。它表示每个采样点用多少位二进制数来表示。
常见的位深有两种:
- 16bit:每个采样点用16位表示,取值范围是-32768~32767
- 32bit float:每个采样点用32位浮点数表示,取值范围非常大
16bit是音频行业的“老黄牛”。CD音质就是16bit/44.1kHz。做回声消除时,16bit也完全够用。
32bit float呢?它最大的好处是——不怕溢出。做算法处理时,经常要做加减乘除。如果用16bit整数,一不小心就溢出了,声音直接爆掉。用32bit float,你随便算,它都能兜住。
注意:32bit float虽然精度高,但数据量是16bit的两倍。在内存受限的嵌入式设备上,要慎重使用。我曾经在一个低端Android平板上做回声消除,用了32bit float,结果内存直接爆了。后来改成16bit,问题解决。
嗯,这里还要提一个坑:Android的AudioRecord默认输出是16bit的PCM数据。但有些第三方库或者硬件抽象层会输出32bit float。你拿到数据后,一定要先检查位深,再做处理。
8.4 声道布局:单声道还是立体声?
声道布局,就是音频数据有几个通道。
做回声消除,99%的情况用单声道。为什么?
- 回声消除只需要一路参考信号和一路麦克风信号
- 立体声会增加算法复杂度,而且效果提升有限
- 大部分手机麦克风就是单声道
但有一种情况要注意:有些Android设备会输出双声道的麦克风数据(左声道是主麦克风,右声道是降噪麦克风)。这时候你要搞清楚,哪一路才是你要处理的信号。
声道布局在数据中的排列方式是这样的:
- 单声道:L L L L L L ...
- 立体声:L R L R L R L R ...
- 5.1声道:L R C LFE Ls Rs ...
做回声消除时,我建议只取左声道(或者你指定的主声道)的数据。把多声道合并成单声道,可以减少计算量,而且不会丢失关键信息。
8.5 知识体系总览
下面这张图,把PCM编码的四个核心要素串起来了。你可以把它当作一个快速参考。
8.6 实战:如何确认音频格式
在Android上,你可以通过AudioRecord的配置来确认音频格式。下面是一段示例代码:
// 配置音频参数
int sampleRate = 16000; // 16kHz
int channelConfig = AudioFormat.CHANNEL_IN_MONO; // 单声道
int audioFormat = AudioFormat.ENCODING_PCM_16BIT; // 16bit
// 创建AudioRecord
int bufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
AudioRecord recorder = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
channelConfig,
audioFormat,
bufferSize
);
// 读取数据
short[] audioBuffer = new short[bufferSize / 2];
recorder.read(audioBuffer, 0, audioBuffer.length);
这段代码创建了一个16kHz、单声道、16bit的音频输入流。读取到的audioBuffer就是PCM数据,每个元素是一个16位的短整型。
小技巧:如果你不确定当前设备的音频格式,可以打印一下AudioRecord.getMinBufferSize()的返回值。如果返回的是负数,说明参数不合法。我经常用这个方法来快速验证配置是否正确。
8.7 总结
PCM编码是音频处理的地基。采样率决定了频率范围,位深决定了动态范围,声道布局决定了通道数量。做回声消除时,记住这个黄金组合:16kHz、16bit、单声道。
你想想看,如果连音频格式都没搞对,后面再牛的算法也是白搭。所以,每次拿到音频数据,先问自己三个问题:采样率是多少?位深是多少?声道布局是什么?
搞清楚这些,咱们才能继续往下走。