4、实时音频处理基础:采样率、位深、帧长、缓冲区管理、乒乓缓冲机制

好,咱们进入第四章。这一章讲的是实时音频处理里最基础、也最绕不开的几个概念。说白了,就是数字音频的“三围”加上怎么管好数据流。我刚开始做音频驱动的时候,觉得这些参数不就是配几个寄存器嘛,后来被坑过几次才明白——这些参数选不对,后面降噪算法写得再好也白搭。

4.1 采样率与位深:数字音频的“分辨率”

先聊采样率。采样率就是每秒从连续信号里抽多少个样,单位是 Hz。根据奈奎斯特定理,采样率至少要是信号最高频率的两倍,才能无失真地重建。人耳能听到的上限大概是 20kHz,所以 CD 音质的 44.1kHz 就是这么来的。

在高通平台上,我常用的采样率有这么几种:

采样率 典型场景 奈奎斯特频率
8 kHz 窄带语音通话 4 kHz
16 kHz 宽带语音、降噪算法常用 8 kHz
44.1 kHz CD 音质音乐 22.05 kHz
48 kHz 视频、专业音频 24 kHz

位深呢,就是每个采样点用多少 bit 来表示。16 bit 有 65536 个量化等级,动态范围大概 96 dB。24 bit 能到 144 dB。做实时降噪的时候,我建议内部处理至少用 32 bit 浮点,避免累加误差。输出的时候再根据硬件能力转成 16 bit 或 24 bit。

我的经验: 在高通平台上做降噪,采样率选 16 kHz 是个不错的平衡点。既能覆盖语音主要频段,又不会让计算量太大。我有个项目一开始用了 48 kHz,结果算法跑不完一帧,后来降到 16 kHz 才稳住。

4.2 帧长:算法处理的“时间切片”

帧长就是每次处理的数据块大小,单位是采样点数。比如帧长 256,在 16 kHz 采样率下,就对应 16 毫秒的音频。

帧长选多少,直接影响延迟和频率分辨率:

  • 帧长短(如 128 点):延迟低,但频率分辨率差,FFT 的 bin 间隔大。
  • 帧长长(如 512 或 1024 点):频率分辨率好,但延迟高,内存占用也大。

做实时降噪,我个人习惯用 256 或 512 点帧长。256 点延迟大概 16 ms,人耳基本感觉不到。512 点降噪效果更好,但延迟会到 32 ms 左右,通话场景就有点勉强了。

注意: 帧长不是越大越好。我曾经在一个项目里为了降噪效果把帧长设到 2048,结果延迟太大,用户说“像在对讲机里说话”。后来改回 512,效果和体验才平衡。

4.3 缓冲区管理:数据流的“蓄水池”

缓冲区就是一块内存,用来暂存音频数据。为什么需要它?因为音频采集和算法处理是异步的。硬件 DMA 往缓冲区里写数据,算法从缓冲区里读数据。如果没有缓冲区,数据一来就得马上处理,系统根本忙不过来。

缓冲区管理有几个关键参数:

  • 缓冲区大小:通常等于帧长,或者帧长的整数倍。
  • 缓冲区数量:至少 2 个,才能实现乒乓操作。
  • 读写指针:记录当前数据写到哪里、读到哪里。

在高通平台上,音频缓冲区通常用 ADSP 侧的共享内存。我一般会申请 2 到 4 个缓冲区,每个大小等于帧长乘以字节数(比如 16 bit 就是 2 字节)。

// 缓冲区初始化示例
#define FRAME_SIZE 256
#define NUM_BUFFERS 4
#define BYTES_PER_SAMPLE 2  // 16 bit

int16_t audio_buffers[NUM_BUFFERS][FRAME_SIZE];
int write_index = 0;
int read_index = 0;

4.4 乒乓缓冲机制:避免数据“打架”

乒乓缓冲,说白了就是两个缓冲区轮流用。一个给硬件写数据,一个给算法读数据。写满一个就切换,读空一个也切换。这样读写互不干扰,不会出现“刚写一半就被读走”的情况。

我画个图帮你理解:

乒乓缓冲机制流程图 音频采集 DMA 缓冲区 A 正在写入 缓冲区 B 等待读取 降噪算法处理 写入 读取 写满切换 工作流程: 1. DMA 向缓冲区 A 写入数据,同时算法从缓冲区 B 读取上一帧数据。 2. 缓冲区 A 写满后,交换角色:DMA 写 B,算法读 A。

你看,乒乓缓冲的核心就是“交替使用”。硬件写一个,算法读另一个。写满就换,读空也换。这样永远不会冲突。

在高通平台上实现乒乓缓冲,我一般用两个指针来管理:

// 乒乓缓冲切换逻辑
int current_write_buf = 0;  // 当前硬件写入的缓冲区索引
int current_read_buf = 1;   // 当前算法读取的缓冲区索引

// 硬件中断回调:一帧数据采集完成
void audio_dma_callback() {
    // 切换写入缓冲区
    current_write_buf = (current_write_buf + 1) % NUM_BUFFERS;
    // 通知算法有新数据
    signal_algorithm();
}

// 算法处理函数
void process_audio() {
    // 从当前读取缓冲区取数据
    int16_t *input = audio_buffers[current_read_buf];
    // 执行降噪处理
    noise_reduction(input, output, FRAME_SIZE);
    // 切换读取缓冲区
    current_read_buf = (current_read_buf + 1) % NUM_BUFFERS;
}
关键点: 乒乓缓冲的缓冲区数量至少是 2 个。用 3 个或 4 个缓冲区可以应对更复杂的场景,比如算法处理时间不稳定时,多一个缓冲区能起到“缓冲溢出”的保护作用。

4.5 高通平台上的实践要点

在高通平台上做实时音频处理,有几个地方要特别注意:

  • DMA 配置:音频 DMA 通常支持循环模式和乒乓模式。我建议用乒乓模式,配合双缓冲区,硬件自动切换。
  • 缓存一致性:ADSP 和 CPU 共享内存时,要注意缓存刷新。我踩过这个坑——数据写进去了,但 CPU 读的是缓存里的旧数据。
  • 中断优先级:音频中断优先级要设高,避免被其他任务打断导致数据丢失。
避坑指南: 我曾经在一个项目里,DMA 配置成了循环模式,但只用了单缓冲区。结果算法处理时间稍微波动,就覆盖了还没处理的数据。后来改成乒乓双缓冲区,问题才解决。记住:单缓冲区 + 循环模式 = 数据覆盖风险。

好了,这一章的内容就这些。采样率、位深、帧长、缓冲区管理、乒乓缓冲——这些概念看起来简单,但它们是实时音频处理的基石。后面讲降噪算法实现的时候,你会发现每一步都离不开这些基础参数的选择。