音频优化:延迟、选择、焦点与回声

音频优化这件事,说实话,在Android系统里经常被低估。大家往往盯着CPU、GPU、内存这些硬指标,觉得音频嘛,能响就行。但真正做过音视频通话、K歌、或者低延迟游戏的人就知道——音频搞不好,体验直接崩盘。

我个人习惯把音频优化分成四个维度:延迟播放器选择焦点管理回声消除。今天咱们一个一个聊透。

音频优化四大核心模块 音频优化 音频延迟优化 AudioTrack vs MediaPlayer 音频焦点管理 回声消除(AEC) 缓冲区大小 · 采样率 · AAudio 低延迟场景 · 资源开销 · API选择 请求 · 放弃 · 混音策略 硬件AEC · 软件AEC · 双讲检测

一、音频延迟优化:从缓冲区到耳朵

音频延迟,说白了就是从你发出声音到别人听到声音的时间差。这个值如果超过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() 看看设备能力,然后再动手改代码。

嗯,音频优化做到位了,用户可能不会夸你——但做不好,用户一定会骂你。这就是音频的「隐形价值」。


专注资料整理