音频优化:延迟、选择、焦点与回声
音频优化这件事,说实话,在Android系统里经常被低估。大家往往盯着CPU、GPU、内存这些硬指标,觉得音频嘛,能响就行。但真正做过音视频通话、K歌、或者低延迟游戏的人就知道——音频搞不好,体验直接崩盘。
我个人习惯把音频优化分成四个维度:延迟、播放器选择、焦点管理、回声消除。今天咱们一个一个聊透。
一、音频延迟优化:从缓冲区到耳朵
音频延迟,说白了就是从你发出声音到别人听到声音的时间差。这个值如果超过100ms,人就能明显感觉到「卡顿」或「不同步」。
我在项目中遇到过最夸张的情况——某款K歌App,用户唱完一句,耳机里要等半秒才听到自己的声音。用户直接骂「这玩意儿是延迟还是回音啊?」。嗯,这就是典型的音频延迟没处理好。
1.1 缓冲区大小是关键
Android音频延迟的罪魁祸首,往往是缓冲区太大。系统为了省电、防卡顿,默认给你一个比较大的缓冲区。但低延迟场景下,我们需要尽可能小的缓冲区。
// 获取最小缓冲区大小
int minBufferSize = AudioTrack.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT
);
// 创建低延迟AudioTrack
AudioTrack track = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRate)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build())
.setBufferSizeInBytes(minBufferSize)
.setPerformanceMode(AudioTrack.PERFORMANCE_MODE_LOW_LATENCY)
.build();
关键点:使用 PERFORMANCE_MODE_LOW_LATENCY 模式,配合 getMinBufferSize() 返回的最小值。但注意,不同设备的最小缓冲区差异很大,从96帧到960帧都有可能。
1.2 采样率与AAudio
我个人建议,如果目标API 26+,直接上AAudio。它比AudioTrack的延迟更低,而且支持共享内存模式,减少了一次数据拷贝。
// AAudio示例(API 26+)
AAudioStreamBuilder *builder;
AAudio_createStreamBuilder(&builder);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, 1);
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_FLOAT);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStream *stream;
AAudioStreamBuilder_openStream(builder, &stream);
我的经验:AAudio在Pixel设备上延迟可以做到10ms以内,但部分国产厂商的驱动实现有bug。建议做兼容性测试,备选方案还是AudioTrack。
二、AudioTrack vs MediaPlayer:选谁?
这个问题我经常被问到。其实选择很简单——看你需要什么级别的控制力。
| 对比维度 | MediaPlayer | AudioTrack |
|---|---|---|
| 使用场景 | 播放本地/网络音频文件 | 实时音频流、PCM数据 |
| 延迟 | 较高(100-300ms) | 较低(可到20-50ms) |
| 控制粒度 | 粗粒度(播放/暂停/跳转) | 细粒度(逐帧写入) |
| 资源开销 | 较低(内部做了缓存优化) | 较高(需要自己管理缓冲区) |
| 格式支持 | MP3、AAC、FLAC等 | 仅PCM(需自行解码) |
说白了,如果你只是放个背景音乐,用MediaPlayer就对了。但如果你在做实时语音、K歌、乐器App,那必须上AudioTrack。
我曾经接手过一个项目,他们用MediaPlayer播放实时合成的音频流,结果延迟飙到400ms。我改成AudioTrack后,延迟直接降到50ms以内。用户反馈从「没法用」变成了「还不错」。
注意:AudioTrack需要你自己处理音频数据的解码、重采样、格式转换。如果处理不当,反而会引入额外延迟。建议在子线程中写入数据,避免阻塞主线程。
三、音频焦点管理:别让App「打架」
音频焦点,说白了就是多个App同时想发声时,谁说了算。这个机制Android从2.2就有了,但很多开发者根本不处理。
我记得有一次测试,打开音乐App再打开导航App,两个声音混在一起,谁也听不清。用户只能手动关掉一个。这就是典型的焦点管理缺失。
3.1 请求与释放焦点
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// 请求焦点
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build())
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChange -> {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// 获得焦点,恢复播放
break;
case AudioManager.AUDIOFOCUS_LOSS:
// 永久失去焦点,停止播放
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
// 暂时失去焦点,暂停播放
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
// 可以降低音量继续播放(俗称「闪避」)
break;
}
})
.build();
int result = audioManager.requestAudioFocus(focusRequest);
最佳实践:通话类App使用 AUDIOFOCUS_GAIN_TRANSIENT,音乐类使用 AUDIOFOCUS_GAIN。导航类建议使用 AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,只降低背景音乐音量,不要完全中断。
3.2 闪避(Ducking)的正确姿势
闪避不是让你直接静音,而是把音量降到原来的30%-50%。我见过有些App收到 LOSS_TRANSIENT_CAN_DUCK 就直接调成0,这其实不对。用户想听导航提示的同时继续听歌,你直接静音反而体验更差。
四、回声消除:别让用户听到自己的「回音」
回声消除(AEC)是实时通信中最头疼的问题之一。说白了,就是扬声器放出来的声音又被麦克风收回去,再传给对方,对方就听到了自己的回声。
我在做某款会议App时,遇到过用户投诉「我说话有回音」。排查了半天,发现是硬件AEC没开启,软件AEC又没做适配。最后两个都没生效,回声自然就传出去了。
4.1 硬件AEC vs 软件AEC
| 方案 | 优点 | 缺点 |
|---|---|---|
| 硬件AEC | 延迟低、效果好、省电 | 依赖设备驱动,部分设备不支持 |
| 软件AEC(如WebRTC AEC3) | 跨平台、可定制、不依赖硬件 | 计算开销大、延迟较高 |
// 开启硬件AEC(如果设备支持)
AudioRecord record = new AudioRecord.Builder()
.setAudioSource(MediaRecorder.AudioSource.VOICE_COMMUNICATION)
.setAudioFormat(new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(16000)
.setChannelMask(AudioFormat.CHANNEL_IN_MONO)
.build())
.setBufferSizeInBytes(bufferSize)
.build();
// 注意:VOICE_COMMUNICATION 音源会自动启用硬件AEC(如果设备支持)
我的建议:优先使用硬件AEC,但一定要做fallback。可以在初始化时检测 AudioRecord.getActiveMicrophones() 和 AudioManager.getProperty(AudioManager.PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED) 来判断设备能力。
4.2 双讲检测(Double Talk Detection)
回声消除最怕什么?最怕两个人同时说话。这时候AEC算法容易把对方的声音也当成回声消掉。所以好的AEC实现必须包含双讲检测。
WebRTC的AEC3在这方面做得不错,它通过分析近端和远端的信号能量比来判断是否处于双讲状态。如果检测到双讲,会降低滤波器的更新速度,避免过度消除。
// WebRTC AEC3 配置示例(伪代码)
Aec3Config config = new Aec3Config();
config.enableDoubleTalkDetection = true;
config.doubleTalkDetectionThreshold = 0.5f; // 阈值,根据实际场景调整
config.echoPathDelayMs = 50; // 回声路径延迟,需要实测
避坑指南:我曾经遇到过一款设备,硬件AEC开启后反而引入了噪声。后来发现是驱动bug,只能关闭硬件AEC,改用软件方案。所以一定要做A/B测试,不要盲目信任硬件AEC。
总结一下
音频优化没有银弹。延迟、播放器选择、焦点管理、回声消除,每个点都需要根据实际场景做权衡。我的习惯是:先测量,再优化。用 dumpsys media.audio_flinger 看看当前延迟,用 AudioManager.getProperty() 看看设备能力,然后再动手改代码。
嗯,音频优化做到位了,用户可能不会夸你——但做不好,用户一定会骂你。这就是音频的「隐形价值」。