3. 移动端音频处理场景
音频采集是整条链路的起点。你想想看,如果源头就卡了,后面再怎么优化也是白搭。我在项目中遇到过不少团队,花了大把精力优化编解码和渲染,结果一查,录音延迟占了总延迟的一半还多。嗯,今天我们就来聊聊怎么把音频采集这第一关打通。
为什么录音延迟这么重要?
说白了,移动端音频处理的核心就是跟时间赛跑。从麦克风采集到数据回调,再到后续处理,每一步都在消耗宝贵的毫秒。我个人的习惯是,先把采集延迟控制在 20ms 以内,再谈其他优化。
延迟主要来自三个地方:
- 硬件缓冲:麦克风到 DSP 芯片的传输
- 驱动层缓冲:操作系统音频服务的数据搬运
- 应用层缓冲:我们开发者能控制的 AudioRecord 或 AudioUnit 缓冲区
前两个我们基本动不了,但第三个——应用层缓冲区大小,是我们调优的主战场。
核心原则:缓冲区越小,延迟越低,但 CPU 负载越高,丢帧风险越大。缓冲区越大,越稳定,但延迟也越大。我们要在两者之间找到平衡点。
Android 端:AudioRecord 参数调优
Android 的音频采集,我建议优先用 AudioRecord。虽然 AAudio 在 Android O 之后表现更好,但兼容性还是 AudioRecord 更稳。我在一个日活千万级的 App 里做过测试,AAudio 在部分低端机上会出现诡异的断流问题,最后还是切回了 AudioRecord。
采样率与通道配置
采样率别乱选。虽然 44100Hz 是 CD 音质,但在移动端,48000Hz 才是最优解。为什么?因为大部分移动设备的硬件原生采样率就是 48000Hz,用 44100Hz 反而会触发重采样,增加延迟和 CPU 开销。
| 采样率 | 延迟表现 | CPU 开销 | 推荐场景 |
|---|---|---|---|
| 8000Hz | 低 | 低 | 语音通话(窄带) |
| 16000Hz | 低 | 低 | 语音通话(宽带) |
| 44100Hz | 中 | 中 | 音乐录制(兼容性) |
| 48000Hz | 低 | 低 | 通用最优解 |
通道配置上,单声道(CHANNEL_IN_MONO)延迟最低。双声道虽然能录立体声,但数据量翻倍,缓冲区处理压力也翻倍。除非你明确需要立体声,否则我建议一律用单声道。
缓冲区大小选择
这是最关键的参数。Android 提供了 getMinBufferSize() 方法,但千万别直接用这个最小值。为什么?因为最小值是为了保证不丢帧,不是为低延迟设计的。
我个人的经验公式是这样的:
// 目标延迟:20ms
int sampleRate = 48000;
int targetLatencyMs = 20;
int channelConfig = AudioFormat.CHANNEL_IN_MONO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
// 计算理论缓冲区大小
int bufferSize = sampleRate * targetLatencyMs / 1000 * 2; // 16bit = 2字节
// 对齐到 getMinBufferSize 的倍数
int minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
int finalBufferSize = ((bufferSize / minBufferSize) + 1) * minBufferSize;
这样算出来的缓冲区,延迟在 20ms 左右,同时保证了兼容性。我在项目中用这个公式,覆盖了从骁龙 660 到骁龙 8 Gen 2 的机型,基本没出过问题。
小技巧:如果发现某些机型有爆音,可以把 targetLatencyMs 调到 30ms。延迟增加 10ms,但稳定性提升明显。我一般会在 App 启动时做一次设备检测,低端机用 30ms,中高端用 20ms。
iOS 端:AudioUnit 参数调优
iOS 这边,AudioQueue 虽然简单,但延迟偏高。要做低延迟,必须上 AudioUnit。我记得第一次用 AudioUnit 时,被它的复杂配置折腾得够呛,但一旦跑通,效果确实好。
AudioUnit 配置要点
核心是配置 kAudioUnitSubType_RemoteIO 的输入部分:
// 配置 AudioComponentDescription
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
// 启用输入
UInt32 enableInput = 1;
AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input, 1, &enableInput, sizeof(enableInput));
// 设置音频格式
AudioStreamBasicDescription asbd;
asbd.mSampleRate = 48000.0;
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
asbd.mBitsPerChannel = 16;
asbd.mChannelsPerFrame = 1;
asbd.mFramesPerPacket = 1;
asbd.mBytesPerFrame = 2;
asbd.mBytesPerPacket = 2;
这里有个坑:mSampleRate 一定要设成 48000。iOS 设备的硬件采样率基本都是 48000,设成 44100 会触发重采样,延迟直接翻倍。我曾经在这个问题上浪费了两天时间,后来用 Instruments 一测才发现是重采样在作怪。
缓冲区大小设置
iOS 的缓冲区大小通过 kAudioSessionProperty_PreferredHardwareIOBufferDuration 来设置。这个值直接决定了延迟:
// 目标延迟:20ms
Float32 bufferDuration = 0.02; // 20ms
AudioSessionSetProperty(kAudioSessionProperty_PreferredHardwareIOBufferDuration,
sizeof(bufferDuration), &bufferDuration);
实际可用的缓冲区大小受硬件限制。iOS 设备支持的典型值如下:
| 设备类型 | 最小缓冲区 | 推荐缓冲区 | 对应延迟 |
|---|---|---|---|
| iPhone 12 及以上 | 5.8ms | 10ms | 10-15ms |
| iPhone X - 11 | 10ms | 20ms | 20-25ms |
| iPad Pro | 5.8ms | 10ms | 10-15ms |
| 旧款设备 | 20ms | 30ms | 30-40ms |
注意:不要直接设成最小值。我在 iPhone 12 上试过 5.8ms,虽然延迟极低,但 CPU 占用率飙升到 40% 以上,而且频繁出现音频卡顿。建议从 20ms 开始,逐步降低,直到出现异常再回调。
缓冲区大小选择的通用策略
不管是 Android 还是 iOS,缓冲区选择都有一些通用原则。我总结了一个决策流程,你可以参考:
避坑指南:我踩过的那些坑
做音频采集优化,有些坑是绕不过去的。我把自己踩过的几个典型问题分享给你:
- 缓冲区大小设成固定值:不同设备的性能差异很大。我曾经在小米 6 上跑得好好的,换到红米 9 上直接爆音。后来改成运行时动态检测设备性能,再选择缓冲区大小。
- 忽略采样率转换:Android 上如果设了 44100Hz,而硬件是 48000Hz,系统会默默做重采样。这个重采样不仅增加延迟,还会引入额外的噪声。用
AudioManager.getProperty(PROPERTY_OUTPUT_SAMPLE_RATE)获取设备原生采样率,然后对齐。 - iOS 上忘记设置 AudioSession Category:如果不设
AVAudioSessionCategoryPlayAndRecord,录音可能无声,或者延迟异常高。我见过一个案例,就是因为 Category 设成了AVAudioSessionCategorySoloAmbient,录音延迟飙到了 100ms 以上。 - 回调线程阻塞:音频回调是实时线程,千万别在里面做 I/O 操作、加锁、或者分配内存。我早期犯过这个错,在回调里写日志文件,结果录音直接卡死。
重要提醒:音频回调线程的优先级很高,但也很脆弱。任何阻塞操作都会导致音频断流。如果你需要在回调里做复杂处理,建议用无锁队列把数据传给工作线程。
实战建议:从 50ms 到 15ms
最后,我分享一个实际项目的优化过程。一个直播 App,初始录音延迟 50ms,用户反馈声音和画面不同步。我们做了三步优化:
- 采样率从 44100Hz 改为 48000Hz:延迟降到 35ms
- 缓冲区从 4096 降到 2048(Android):延迟降到 25ms
- 启用低延迟模式(iOS 设 bufferDuration 为 0.01):延迟降到 15ms
整个过程花了三天时间,但效果立竿见影。用户反馈延迟问题基本消失。嗯,这就是调优的魅力——有时候改动不大,但效果显著。
记住,音频采集是整条链路的起点。把这一关打通了,后面的编解码、渲染优化才能发挥最大价值。