12、音频解码优化:AudioTrack低延迟播放、OpenSL ES最佳实践、音频焦点与混音处理
音频解码优化,说白了就是让声音更快、更稳、更听话地播放出来。我做了这么多年音视频,发现很多开发者把精力都放在编解码上,结果到了播放环节反而拖了后腿。今天咱们就聊聊播放链路上的三个关键点:AudioTrack的低延迟玩法、OpenSL ES的正确姿势,还有音频焦点和混音那些坑。
一、AudioTrack低延迟播放
AudioTrack是Android原生提供的音频播放API。很多人觉得它简单,new一个对象,write数据就完事了。但你要追求低延迟,这里面的门道可不少。
1.1 选择合适的音频属性
我见过不少项目,上来就用默认参数创建AudioTrack。结果延迟动不动就200ms+,用户一说话就感觉声音对不上嘴型。
关键参数就两个:音频流类型和性能模式。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| streamType | AudioManager.STREAM_VOICE_CALL | 低延迟场景首选,系统会优先处理 |
| performanceMode | AudioAttributes.USAGE_VOICE_COMMUNICATION | 告诉系统我要低延迟,别给我加缓冲 |
核心代码示例:
AudioAttributes attributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFormat format = new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(44100)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build();
AudioTrack audioTrack = new AudioTrack.Builder()
.setAudioAttributes(attributes)
.setAudioFormat(format)
.setBufferSizeInBytes(getOptimalBufferSize())
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
1.2 缓冲区大小是关键
缓冲区设大了,延迟高;设小了,容易卡顿。我个人的习惯是:先查系统最小值,再根据场景微调。
int minBufferSize = AudioTrack.getMinBufferSize(44100,
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.ENCODING_PCM_16BIT);
// 实际使用可以稍微大一点,防止卡顿
int bufferSize = minBufferSize * 2;
嗯,这里要注意:getMinBufferSize返回的是系统保证不卡顿的最小值。但不同手机差异很大,我遇到过一些低端机,用最小值写数据时偶尔会爆音。所以建议在minBufferSize基础上乘个1.5到2倍。
1.3 写入策略
用write()方法时,尽量用阻塞模式。非阻塞模式虽然看起来灵活,但容易导致数据没写完就被覆盖了。
// 阻塞写入,确保数据完整
int written = audioTrack.write(audioData, 0, audioData.length);
if (written != audioData.length) {
// 处理写入不完整的情况
Log.w("AudioTrack", "写入不完整: " + written + "/" + audioData.length);
}
避坑指南:我曾经在一个直播项目中,用了非阻塞模式配合回调去写数据。结果在华为某款手机上,声音断断续续的。后来改成阻塞模式,问题就解决了。说白了,非阻塞模式适合对延迟极度敏感、且能容忍丢帧的场景。一般场景用阻塞模式更稳。
二、OpenSL ES最佳实践
OpenSL ES是Android上更底层的音频API。它比AudioTrack更接近硬件,延迟可以做到更低。但代价是——API设计得比较反人类。
2.1 初始化流程
OpenSL ES的初始化流程比较固定,我一般封装成一个工具类。核心步骤就三步:
- 创建引擎:slCreateEngine,全局只创建一次
- 创建播放器:配置数据源和输出混音器
- 设置回调:注册缓冲队列回调,处理数据
// 伪代码,展示核心流程
SLObjectItf engineObject;
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
engineObject->Realize(engineObject, SL_BOOLEAN_FALSE);
// 获取引擎接口
SLEngineItf engineEngine;
engineObject->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
// 创建输出混音器
SLObjectItf outputMixObject;
engineEngine->CreateOutputMix(engineEngine, &outputMixObject, 0, NULL, NULL);
outputMixObject->Realize(outputMixObject, SL_BOOLEAN_FALSE);
// 创建播放器
SLObjectItf playerObject;
engineEngine->CreateAudioPlayer(engineEngine, &playerObject, ...);
playerObject->Realize(playerObject, SL_BOOLEAN_FALSE);
2.2 缓冲队列管理
OpenSL ES用的是双缓冲机制。你想想看,如果只有一个缓冲区,写数据和播放数据就会互相打架。双缓冲就是让一个缓冲区在播放,另一个在填充数据。
我建议用三缓冲,多一个缓冲区做冗余。特别是在处理网络音频流时,网络抖动会导致数据到达时间不确定。三缓冲能有效减少卡顿。
缓冲队列配置:
#define BUFFER_COUNT 3
#define BUFFER_SIZE 4096
char* buffers[BUFFER_COUNT];
for (int i = 0; i < BUFFER_COUNT; i++) {
buffers[i] = (char*)malloc(BUFFER_SIZE);
}
// 注册回调
SLBufferQueueItf bufferQueue;
(*playerPlay)->RegisterCallback(bufferQueue, AudioCallback, this);
// 回调函数中填充数据
void AudioCallback(SLBufferQueueItf caller, void* context) {
// 从网络或文件读取数据到缓冲区
// 然后调用 Enqueue 放回队列
(*caller)->Enqueue(caller, buffer, dataSize);
}
2.3 延迟优化技巧
OpenSL ES的延迟可以做到10ms以内,但需要一些技巧:
- 使用低延迟配置:创建播放器时,设置
SL_ANDROID_PERFORMANCE_LATENCY为SL_ANDROID_PERFORMANCE_LATENCY_HIGH(注意:这里命名有点反直觉,HIGH表示低延迟模式) - 采样率匹配:尽量用设备原生支持的采样率,避免重采样带来的延迟
- 减少回调中的操作:回调里只做数据拷贝和入队,别做解码、网络请求等耗时操作
注意:OpenSL ES在Android 5.0以上才支持低延迟模式。如果你要兼容老设备,建议降级到AudioTrack。我曾经在一个项目中没注意这点,结果在Android 4.4的设备上直接崩溃了。
三、音频焦点与混音处理
音频焦点,说白了就是管理多个App同时播放声音时的优先级。比如你在听音乐,突然来个电话,音乐应该暂停。这就是音频焦点在起作用。
3.1 请求和释放焦点
Android的音频焦点机制其实挺完善的。我建议在播放开始前请求焦点,播放结束后释放焦点。
AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
// 请求焦点
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(attributes)
.setOnAudioFocusChangeListener(focusChangeListener)
.build();
int result = audioManager.requestAudioFocus(focusRequest);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
// 焦点获取失败,暂时不要播放
return;
}
// 播放结束后释放焦点
audioManager.abandonAudioFocusRequest(focusRequest);
3.2 焦点变化处理
焦点变化时,系统会回调onAudioFocusChange。常见的几种情况:
| 焦点变化类型 | 建议处理方式 |
|---|---|
| AUDIOFOCUS_GAIN | 恢复播放,音量恢复正常 |
| AUDIOFOCUS_LOSS | 停止播放,释放资源 |
| AUDIOFOCUS_LOSS_TRANSIENT | 暂停播放,不释放资源 |
| AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK | 降低音量(duck),继续播放 |
个人经验:对于CAN_DUCK场景,我习惯把音量降到原来的30%。有些App直接静音,用户体验其实不好。比如导航App,降低音量继续播报,比突然中断要自然得多。
3.3 混音处理
混音,就是把多个音频流混合成一个输出。Android系统本身会做混音,但如果你要自己做(比如游戏中的多个音效叠加),就得注意了。
最简单的混音就是加法混音:
short* mixBuffer = new short[bufferSize];
for (int i = 0; i < bufferSize; i++) {
int sample = (int)buffer1[i] + (int)buffer2[i];
// 防止溢出,做钳位处理
if (sample > 32767) sample = 32767;
if (sample < -32768) sample = -32768;
mixBuffer[i] = (short)sample;
}
嗯,这里要注意:直接加法容易导致溢出,产生爆音。我一般会先做音量归一化,把每个音源音量降到50%左右再相加。或者用除法混音:
// 除法混音,避免溢出
for (int i = 0; i < bufferSize; i++) {
mixBuffer[i] = (short)((buffer1[i] + buffer2[i]) / 2);
}
除法混音虽然安全,但整体音量会下降。所以实际项目中,我更喜欢用自适应混音——根据当前样本值动态调整系数。
四、知识体系总览
说了这么多,咱们用一张图来梳理一下本章的核心内容:
这张图把咱们今天讲的内容串起来了。左边是AudioTrack的优化点,中间是OpenSL ES的核心实践,右边是音频焦点和混音处理。三者共同构成了一个完整的音频播放优化方案。
最后说一句:音频优化没有银弹。不同手机、不同场景,最优方案都不一样。我建议你在项目初期就把这些方案都实现一遍,然后针对你的目标机型做压测。哪个方案延迟最低、最稳定,就用哪个。