4. 音频采集与播放:AudioRecord、AudioTrack、OpenSL ES、AAudio的API使用、配置与回调机制

在Android音视频系统架构中,音频采集与播放是数据链路的起点与终点。无论是VoIP通话、直播推流、语音识别,还是音乐播放器、游戏音效,底层都依赖系统提供的音频API。Android平台历经多年演进,目前存在四套并行的音频I/O方案:传统Java层的AudioRecord/AudioTrack、高性能原生层的OpenSL ES、以及新一代的AAudio。理解这四者的设计哲学、配置参数与回调机制,是构建低延迟、高稳定音频系统的核心能力。本章将从API选型、配置流程、回调模型、工程实践四个维度展开深度剖析。

Android音频采集与播放API架构对比与回调链路 应用层(Java/Kotlin/Native) 业务逻辑:采集回调、播放填充、状态机管理、音频数据路由 AudioRecord Java层采集 read()阻塞/轮询 API 3+ AudioTrack Java层播放 write()阻塞/轮询 API 3+ OpenSL ES Native层采集/播放 回调+队列 API 9+ AAudio Native层采集/播放 回调+阻塞双模式 API 26+ HAL层(Hardware Abstraction Layer) AudioFlinger / AudioHAL / 混音器 / 路由策略 内核驱动与硬件 ALSA / TinyALSA / I2S / TDM / DMIC / Codec / 功放 回调机制: AudioRecord: setRecordPositionUpdateListener AAudio回调: AAudioStream_requestStart + 数据回调

上图展示了四套API在Android音频架构中的位置与核心特征。AudioRecord/AudioTrack位于Java框架层,通过JNI调用Native服务;OpenSL ES与AAudio则直接暴露Native接口,减少跨层开销。从延迟角度看,AudioTrack在旧版本上延迟可达100ms以上,而AAudio在Pixel系列设备上可低至20ms以内。选型时需综合考虑应用场景、最低支持版本、性能要求与维护成本。

4.1 核心原理:四套API的设计哲学与数据通路

理解API的本质差异,需要从数据流的角度切入。音频采集链路为:麦克风→Codec ADC→I2S/TDM总线→DMA Buffer→ALSA驱动→AudioFlinger→应用缓冲区。播放链路则反向。四套API的差异主要体现在缓冲区管理与线程模型上。

  • AudioRecord/AudioTrack:基于Java层封装,内部通过JNI调用Native AudioFlinger。采集时使用read()阻塞式读取,播放时使用write()阻塞式写入。缓冲区由系统分配,应用侧通过setBufferSizeInBytes()指定大小。优点是兼容性极佳,缺点是延迟较高且无法精细控制回调时机。
  • OpenSL ES:基于C语言的Native API,采用BufferQueue模型。应用创建AudioRecorder或AudioPlayer对象,注册回调函数,系统在缓冲区填满或耗尽时触发回调。开发者需要手动管理Buffer的入队与出队,灵活性高但复杂度也高。
  • AAudio:Android 8.0引入的现代Native API,设计目标为低延迟与高可控性。它支持两种模式:共享模式(Shared)与独占模式(Exclusive)。独占模式下应用直接与HAL通信,绕过AudioFlinger混音,延迟最低。回调模型基于AAudioStream_dataCallback,在音频线程中执行,要求回调函数必须快速返回,禁止阻塞操作。
关键区别:AudioRecord/AudioTrack是“拉/推”模型,应用主动读写;OpenSL ES与AAudio是“回调”模型,系统主动通知。回调模型天然适合低延迟场景,但要求开发者严格遵守线程约束。

4.2 配置参数与操作流程:从初始化到启动

无论使用哪套API,核心配置参数都包括:采样率(SampleRate)、声道数(Channel)、音频格式(Format)、缓冲区大小(BufferSize)。以下分别给出各API的典型配置流程。

4.2.1 AudioRecord采集配置

// 1. 计算最小缓冲区
int minBufSize = AudioRecord.getMinBufferSize(44100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT);
// 2. 初始化AudioRecord
AudioRecord record = new AudioRecord(
        MediaRecorder.AudioSource.MIC,
        44100,
        AudioFormat.CHANNEL_IN_MONO,
        AudioFormat.ENCODING_PCM_16BIT,
        minBufSize * 2); // 通常设为最小值的2倍
// 3. 启动采集
record.startRecording();
// 4. 循环读取
byte[] buffer = new byte[minBufSize];
while (isRecording) {
    int read = record.read(buffer, 0, buffer.length);
    // 处理PCM数据
}
// 5. 停止与释放
record.stop();
record.release();

注意:read()是阻塞调用,若缓冲区未填满,线程会挂起。在实时性要求高的场景(如VoIP),建议将读取操作放在独立线程,并使用setRecordPositionUpdateListener监听位置更新,实现非阻塞式采集。

4.2.2 AAudio播放配置

AAudioStreamBuilder *builder;
AAudioStream *stream;
// 1. 创建Builder
AAudio_createStreamBuilder(&builder);
// 2. 配置参数
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
AAudioStreamBuilder_setSampleRate(builder, 48000);
AAudioStreamBuilder_setChannelCount(builder, 2);
AAudioStreamBuilder_setFormat(builder, AAUDIO_FORMAT_PCM_I16);
AAudioStreamBuilder_setBufferCapacityInFrames(builder, 960);
// 3. 设置数据回调
AAudioStreamBuilder_setDataCallback(builder, myDataCallback, userData);
// 4. 打开流
AAudioStreamBuilder_openStream(builder, &stream);
// 5. 启动
AAudioStream_requestStart(stream);
// 回调函数示例
aaudio_data_callback_result_t myDataCallback(
        AAudioStream *stream, void *userData, void *audioData, int32_t numFrames) {
    // 将PCM数据填充到audioData,长度 = numFrames * channelCount * sizeof(int16_t)
    return AAUDIO_CALLBACK_RESULT_CONTINUE;
}

AAudio的BufferCapacityInFrames决定了最大缓冲帧数,而实际缓冲区大小由系统根据延迟需求动态调整。开发者可通过AAudioStream_getFramesPerBurst()获取每次回调的帧数,通常为128或256帧。

配置项AudioRecord/TrackOpenSL ESAAudio
采样率构造时指定SLDataFormat_PCMBuilder设置
缓冲区控制setBufferSizeInBytesBufferQueue手动管理setBufferCapacityInFrames
回调触发Listener(可选)必选回调必选回调
延迟优化一般较好最优(独占模式)
最低API3926

4.3 回调机制深度拆解:线程模型与数据一致性

回调机制是高性能音频系统的核心。AAudio与OpenSL ES的回调运行在实时音频线程中,该线程优先级极高,任何阻塞操作(如锁、内存分配、I/O)都可能导致音频卡顿甚至系统重启。开发者必须遵守以下铁律:

  1. 禁止阻塞:回调内不得调用sleep()、mutex.lock()、malloc()等操作。
  2. 数据拷贝:回调中应尽快将音频数据拷贝到应用缓冲区,然后立即返回。
  3. 无锁队列:跨线程传递数据时,使用无锁环形队列(如boost::lockfree::spsc_queue)或原子变量。
  4. CPU亲和性:音频线程通常绑定到特定CPU核心,避免被调度器迁移。
常见陷阱:在AAudio回调中直接调用AAudioStream_write()会导致死锁。回调模式与阻塞模式不能混用。若需在回调中停止流,应使用AAudioStream_requestStop()的异步版本,而非同步等待。

4.4 实战案例:构建低延迟VoIP音频采集模块

假设我们需要实现一个VoIP通话模块,要求端到端延迟低于80ms,且需兼容Android 8.0以下设备。技术选型为:AAudio作为主路径(API 26+),OpenSL ES作为降级路径(API 21-25),AudioRecord作为最后兜底(API 21以下)。

初始现象与目标:在Pixel 6设备上测试,使用AudioRecord采集延迟约120ms,无法满足VoIP要求。改用AAudio独占模式后,采集延迟降至25ms,播放延迟降至30ms,整体链路满足要求。

关键参数与操作步骤:

  1. 查询设备是否支持独占模式:AAudioStream_isMMapSupported()。
  2. 设置AAUDIO_PERFORMANCE_MODE_LOW_LATENCY性能模式。
  3. 使用AAudioStream_getFramesPerBurst()确定回调帧数,通常为192帧(48kHz采样率下约4ms)。
  4. 在回调中直接进行PCM编码(如Opus),避免额外拷贝。
  5. 对于API 21-25设备,使用OpenSL ES的AndroidSimpleBufferQueue接口,注册SlAndroidSimpleBufferQueueItf回调。

结果对比:在Pixel 6上,AAudio独占模式采集延迟25ms,播放延迟30ms;OpenSL ES延迟约45ms;AudioRecord延迟约120ms。在低端设备(如Redmi 9A)上,AAudio可能不支持独占模式,自动降级为共享模式,延迟约60ms,仍优于AudioRecord。

风险与代价:AAudio独占模式在部分设备上存在兼容性问题(如某些三星机型),需在初始化时捕获异常并降级。此外,独占模式会占用唯一的低延迟音频通道,若同时播放铃声或导航音,可能被系统抢占。

复盘经验:音频API选型不能只看延迟指标,还需考虑设备覆盖率和系统资源竞争。建议采用“AAudio优先+OpenSL ES降级+AudioRecord兜底”的三级策略,并在初始化时通过AAudioStream_getPerformanceMode()确认实际生效模式。

4.5 常见误区与总结复盘

在多年音频开发中,以下误区反复出现,值得警惕:

  • 误区一:缓冲区越大越稳定。实际上,过大的缓冲区会显著增加延迟,且不会提升稳定性。正确做法是使用getMinBufferSize()作为起点,再根据实际卡顿情况微调。
  • 误区二:回调中做格式转换。格式转换(如PCM→AAC)耗时较长,应放在工作线程。回调中只做内存拷贝。
  • 误区三:忽略采样率匹配。如果采集为44.1kHz,播放为48kHz,必须进行重采样。在AAudio中,可通过AAudioStreamBuilder_setSampleRateConversionQuality()设置重采样质量,但最好在应用层统一采样率。
  • 误区四:不处理音频焦点变化。VoIP应用应监听AudioManager.OnAudioFocusChangeListener,在失去焦点时暂停采集,避免后台录音。

总结而言,Android音频I/O的选型与实现,本质是在延迟、兼容性、CPU占用、开发复杂度之间做权衡。AudioRecord/AudioTrack适合对延迟不敏感的应用(如录音机、音乐播放);OpenSL ES适合需要Native处理且需兼容旧版本的应用;AAudio是当前低延迟场景的首选,但需做好降级预案。掌握回调机制与线程模型,是避免音频卡顿、崩溃的关键。建议开发者在项目初期就建立音频延迟测试基准,使用dumpsys media.audio_flinger查看实际缓冲区大小,持续优化。