AAudio

AAudio 是在 Android O 版本中引入的全新 Android C API。此 API 专为需要低延迟的高性能音频应用而设计。应用通过读取数据并将数据写入流来与 AAudio 进行通信。

AAudio API 采用最精简的设计,不执行以下功能:

  • 音频设备枚举
  • 音频端点之间的自动化路由
  • 文件 IO
  • 解码压缩的音频
  • 在单一回调中自动呈交所有输入/流。

开始使用

您可以通过 C++ 代码调用 AAudio。若要向应用添加 AAudio 功能集,请添加 AAudio.h 头文件:

#include <aaudio/AAudio.h>

音频流

AAudio 在您的应用与 Android 设备的音频输入端及输出端之间移动音频数据。您的应用通过读取和写入以 AAudioStream 结构表示的音频流来传入和传出数据。这些读取/写入调用可以是阻塞式调用或非阻塞式调用。

流由下列各项定义:

  • 音频设备 - 作为流中数据的来源或接收器。
  • 共享模式 - 用于确定流是否对本来可在多个流之间共享的音频设备进行独占访问。
  • 流中音频数据的格式。

音频设备

每个流都连接到单个音频设备。

音频设备是硬件接口或虚拟端点,用作连续的数字音频数据流的来源或接收器。请勿将音频设备(内置麦克风或蓝牙耳机)与运行应用的 Android 设备(手机或智能手表)混淆。

您可使用 AudioManager 方法 getDevices() 来发现 Android 设备上可用的音频设备。该方法会返回每个设备 type 的相关信息。

Android 设备上的每个音频设备都具有唯一 ID。您可使用该 ID 将音频流与特定音频设备绑定。但在大多数情况下,您可以让 AAudio 选择默认的主要设备,无需自己指定。

连接到流的音频设备负责确定该流是用于输入还是输出。流只能在一个方向上移动数据。定义流时,您还可以设置其方向。打开流时,Android 会执行检查,确保音频设备与流方向一致。

共享模式

流具有共享模式:

  • AAUDIO_SHARING_MODE_EXCLUSIVE 表示该流对其音频设备进行独占访问;该设备不可供任何其他音频流使用。如果音频设备已在使用当中,流可能无法对其进行独占访问。独占流的延迟时间往往较短,但连接断开的可能性也较大。如果不再需要独占流,应尽快予以关闭,以便其他应用访问该设备。独占流可以最大限度缩短延迟时间。
  • AAUDIO_SHARING_MODE_SHARED 允许 AAudio 混合音频。AAudio 会将分配给同一设备的所有共享流混合。

您可以在创建流时明确设置共享模式。默认情况下,共享模式为 SHARED

音频格式

通过流传递的数据具有常规的数字音频属性,如下所示:

  • 样本数据格式
  • 通道计数(每帧样本数)
  • 采样率

AAudio 支持下列样本格式:

aaudio_format_t C 数据类型 备注
AAUDIO_FORMAT_PCM_I16 int16_t 通用 16 位样本,Q0.15 格式
AAUDIO_FORMAT_PCM_FLOAT 浮点数 -1.0 至 +1.0
AAUDIO_FORMAT_PCM_I24_PACKED 3 个群组中的 uint8_t 打包的 24 位样本,Q0.23 格式
AAUDIO_FORMAT_PCM_I32 int32_t 通用 32 位样本,Q0.31 格式
AAUDIO_FORMAT_IEC61937 uint8_t 针对 HDMI 或 S/PDIF 透传封装在 IEC61937 中的压缩音频

如果您请求特定的样本格式,则流会采用该格式,即使该格式并非设备的最优选项。如果您未指定样本格式,AAudio 会选择最合适的格式。 打开流后,您必须查询样本数据格式,然后根据需要转换数据,如下例所示:

aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
     convertFloatToPcm16(...)
}

创建音频流

AAudio 库遵循构建器设计模式,并提供 AAudioStreamBuilder。

  1. 创建 AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. 请使用与流参数对应的构建器函数,在构建器中设置音频流配置。可供使用的可选设置函数如下所示:

    AAudioStreamBuilder_setDeviceId(builder, deviceId);
    AAudioStreamBuilder_setDirection(builder, direction);
    AAudioStreamBuilder_setSharingMode(builder, mode);
    AAudioStreamBuilder_setSampleRate(builder, sampleRate);
    AAudioStreamBuilder_setChannelCount(builder, channelCount);
    AAudioStreamBuilder_setFormat(builder, format);
    AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);
    

    请注意,这些方法不会报告诸如常量未定义或值超出范围之类的错误。

    如果您未指定 deviceId,将默认为主要输出设备。如果您未指定流方向,将默认为输出流。对于所有其他参数,您可明确设置值,也可以完全不指定参数或将其设置为 AAUDIO_UNSPECIFIED,让系统分配最佳值。

    为安全起见,在创建音频流之后,请检查其状态,如下面的第 4 步所示。

  3. 配置 AAudioStreamBuilder 之后,用其创建流:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

  4. 创建流之后,验证其配置。如果您已指定样本格式、采样率或每帧样本数,这些设置不会变更。如果您已指定共享模式或缓冲区容量,这些设置可能会变更,具体取决于流的音频设备能力,以及运行该流的 Android 设备。作为一种良好的防御性编程习惯,您应该先检查流的配置,然后再使用。您可使用相应函数检索与每项构建器设置对应的流设置:

    AAudioStreamBuilder_setDeviceId() AAudioStream_getDeviceId()
    AAudioStreamBuilder_setDirection() AAudioStream_getDirection()
    AAudioStreamBuilder_setSharingMode() AAudioStream_getSharingMode()
    AAudioStreamBuilder_setSampleRate() AAudioStream_getSampleRate()
    AAudioStreamBuilder_setChannelCount() AAudioStream_getChannelCount()
    AAudioStreamBuilder_setFormat() AAudioStream_getFormat()
    AAudioStreamBuilder_setBufferCapacityInFrames() AAudioStream_getBufferCapacityInFrames()

  5. 您可以保存该构建器,以便将来再用其创建更多流。但是,如果您不打算再使用该构建器,则应将其删除。

    AAudioStreamBuilder_delete(builder);
    

使用音频流

状态转换

AAudio 流一般有以下五种稳定状态(本部分结尾将介绍错误状态 Disconnected):

  • 打开
  • 已开始
  • 已暂停
  • 已刷新
  • 已停止

仅当流处于“已开始”状态时,数据才会通过流来流动。如需转换流所处的状态,请使用以下其中一个函数请求状态转换:

aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);

请注意,您只能针对输出流,请求暂停或刷新:

这些函数是异步函数,因此状态不会立即变更。当您请求变更状态时,流会进入以下相应的过渡状态:

  • 正在开始
  • 正在暂停
  • 正在刷新
  • 正在停止
  • 正在关闭

以下状态图将稳定状态显示为圆角矩形,而将过渡状态显示为虚线矩形。尽管未显示,但您可从任意状态调用 close()

AAudio 生命周期

AAudio 未提供回调函数来提醒您状态发生变更。您可使用特殊函数 AAudioStream_waitForStateChange(stream, inputState, nextState, timeout) 等待状态变更。

此函数本身并不会检测状态变更情况,也不会等待特定的状态,而是等待当前状态偏离您指定的 inputState

例如,请求暂停后,流应立即进入“正在暂停”过渡状态,并在稍后某个时刻进入“已暂停”状态,但不保证一定如此。由于无法等待“已暂停”状态,请使用 waitForStateChange() 来等待除“正在暂停”之外的任何状态。方法如下:

aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;
aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;
int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;
result = AAudioStream_requestPause(stream);
result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);

如果流的状态并非“正在暂停”(即 inputState,我们假定这就是当前执行调用时的状态),该函数会立即返回。否则,函数会阻止运行,直至状态不再是“正在暂停”,或者超时。当函数返回时,参数 nextState 会显示流的当前状态。

您可在调用开始、停止或刷新请求后使用这种方法,将相应的过渡状态用作 inputState。不要在调用 AAudioStream_close() 之后调用 waitForStateChange(),因为流在关闭时会立即被删除。此外,不要在另一线程运行 waitForStateChange() 时调用 AAudioStream_close()

读取和写入音频流

在流启动后,可通过两种方法来处理流中的数据:

对于传输指定帧数的阻塞读取或写入操作,请将 timeoutNanos 设置为大于零。 对于非阻塞调用,请将 timeoutNanos 设置为零。在这种情况下,结果将是传输的实际帧数。

读取输入值时,您应验证是否已读取正确数量的帧。如果未读取正确数量的帧,缓冲区可能包含未知的数据,从而引起音频干扰。您可以在缓冲区中填入零,以产生静音效果:

aaudio_result_t result =
    AAudioStream_read(stream, audioData, numFrames, timeout);
if (result < 0) {
  // Error!
}
if (result != numFrames) {
  // pad the buffer with zeros
  memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
      sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
}

您可以先准备好流的缓冲区,再通过将数据写入或静音写入其中来启动流。此操作必须在 timeoutNanos 设置为零的情况下,通过非阻塞调用来完成。

缓冲区中的数据必须与 AAudioStream_getDataFormat() 所返回的数据格式匹配。

关闭音频流

使用完流之后,请将其关闭:

AAudioStream_close(stream);

关闭流之后,便无法将其与任何基于流的 AAudio 函数配合使用。

断开连接的音频流

如果发生以下任一事件,音频流随时可能会断开连接:

  • 关联的音频设备不再处于连接状态(例如,在拔出头戴式耳机时)。
  • 发生内部错误。
  • 某音频设备不再是主要音频设备。

流断开连接时,其状态为“已断开连接”,所有尝试执行 AAudioStream_write 或其他函数的操作都会返回错误。无论是什么错误代码,您一律须停止并关闭已断开连接的流。

如果您使用的是数据回调(而不是某个直接读取/写入方法),那么当流断开连接时,您不会收到任何返回代码。如需在发生这种情况时接收通知,请编写 AAudioStream_errorCallback 函数,然后使用 AAudioStreamBuilder_setErrorCallback() 注册该函数。

如果您在错误回调线程中收到连接已断开的通知,则流的停止和关闭必须从其他线程中完成。否则可能出现死锁。

请注意,如果打开新的流,其设置可能与原始流不同(例如,framesPerBurst):

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error) {
    // Launch a new thread to handle the disconnect.
    std::thread myThread(my_error_thread_proc, stream, userData);
    myThread.detach(); // Don't wait for the thread to finish.
}

优化性能

您可以通过调整内部缓冲区,以及使用特殊的高优先级线程,优化音频应用的性能。

调整缓冲区以最大限度减少延迟时间

AAudio 会将数据传入其维护的内部缓冲区,并从中传出数据(每个音频设备各有一个内部缓冲区)。

缓冲区的容量是缓冲区中可以存放的数据总量。您可以调用 AAudioStreamBuilder_setBufferCapacityInFrames() 来设置容量。此方法将可分配的容量限制为设备允许的最大值。使用 AAudioStream_getBufferCapacityInFrames() 可以验证缓冲区的实际容量。

应用不必使用缓冲区的全部容量。您可以设置 AAudio 填充缓冲区空间的大小上限。缓冲区空间大小不得超过其容量,而且通常小于其容量。您可以通过控制缓冲区空间大小,确定填充缓冲区所需的脉冲串数,从而控制延迟时间。您可以使用 AAudioStreamBuilder_setBufferSizeInFrames()AAudioStreamBuilder_getBufferSizeInFrames() 方法来处理缓冲区空间大小。

应用播放音频时,会将数据写入缓冲区并阻止运行,直至写入完成。AAudio 以离散的脉冲串从缓冲区中读取数据。每个脉冲串都包含多个音频帧,而且通常小于所读取的缓冲区大小。脉冲串的大小及速率由系统控制,而这些属性通常由音频设备的电路指定。虽然您无法更改脉冲串的大小或速率,但可以根据内部缓冲区所含的脉冲串数量来设置内部缓冲区大小。通常,当 AAudioStream 的缓冲区大小是所报告脉冲串大小的倍数时,延迟时间最短。

      AAudio 缓冲

优化缓冲区空间大小的一种方法是从较大的缓冲区开始,逐渐将其减小直至开始出现缓冲区不足现象,再稍稍将其调大。此外,您也可以从较小的缓冲区空间大小开始,如果出现缓冲区不足现象,则增大缓冲区空间大小,直至输出再次流畅为止。

这个过程推进速度很快,很可能在用户开始播放第一个音频前就已完成。您可以先以静音执行初始缓冲区大小调整,确保用户不会听到任何音频干扰声。随着时间推移,系统性能可能会有所变化(例如,用户可能会关闭飞行模式)。因为缓冲区调整所产生的开销非常小,应用可以在对流读取或写入数据的同时,连续不断地调整缓冲区。

以下是缓冲区优化循环的示例:

int32_t previousUnderrunCount = 0;
int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);

int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);

while (go) {
    result = writeSomeData();
    if (result < 0) break;

    // Are we getting underruns?
    if (bufferSize < bufferCapacity) {
        int32_t underrunCount = AAudioStream_getXRunCount(stream);
        if (underrunCount > previousUnderrunCount) {
            previousUnderrunCount = underrunCount;
            // Try increasing the buffer size by one burst
            bufferSize += framesPerBurst;
            bufferSize = AAudioStream_setBufferSize(stream, bufferSize);
        }
    }
}

对于输入流来说,使用这种方法优化缓冲区大小并无益处。输入流以尽可能快的速度运行,以尝试将缓存数据量保持在最低限度,然后在应用被抢占时填补缓冲区。

使用高优先级回调

如果您的应用从原始线程中读取或写入音频数据,可能会被抢占或遇到定时抖动,进而可能引起音频干扰。使用较大的缓冲区有助于避免此类干扰,但是如果缓冲区较大,音频延迟时间也会更长。对于要求延迟时间较短的应用,音频流可以使用一个异步回调函数,将数据传输到您的应用并从中传输数据。AAudio 会在优先级较高的线程中执行该回调,这有助于改善性能。

该回调函数的原型如下所示:

typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames);

使用流构建方式来注册回调:

AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);

在最简单的情况下,流会定期执行该回调函数,以获取用于下一个脉冲串的数据。

该回调函数不应对调用它的流执行读取或写入操作。如果该回调属于某个输入流,那么您的代码应处理在 audioData 缓冲区(指定为第三个参数)中提供的数据。如果该回调属于某个输出流,那么您的代码应将数据放入该缓冲区。

例如,您可以使用回调来连续生成正弦波输出,如下所示:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    int64_t timeout = 0;

    // Write samples directly into the audioData array.
    generateSineWave(static_cast<float *>(audioData), numFrames);
    return AAUDIO_CALLABCK_RESULT_CONTINUE;
}

使用 AAudio 可以处理多个流。您可以将其中一个流用作主流,并在用户数据中传递指向其他流的指针。针对主流注册回调。然后,对其他流使用非阻塞 I/O。以下是将输入流传递到输出流的往返回调示例。主调用流是输出流。输入流包括在用户数据中。

该回调从输入流执行非阻塞读取,以将数据放入输出流的缓冲区:

aaudio_data_callback_result_t myCallback(
        AAudioStream *stream,
        void *userData,
        void *audioData,
        int32_t numFrames) {
    AAudioStream *inputStream = (AAudioStream *) userData;
    int64_t timeout = 0;
    aaudio_result_t result =
        AAudioStream_read(inputStream, audioData, numFrames, timeout);

  if (result == numFrames)
      return AAUDIO_CALLABCK_RESULT_CONTINUE;
  if (result >= 0) {
      memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0,
          sizeof(sample_type) * (numFrames - result) * samplesPerFrame);
      return AAUDIO_CALLBACK_RESULT_CONTINUE;
  }
  return AAUDIO_CALLBACK_RESULT_STOP;
}

请注意,在此示例中,假定输入流和输出流的通道数量、格式和采样率均相同。流的格式可以不匹配,只要代码正确处理转换即可。

设置性能模式

每个 AAudioStream 都具有性能模式,而这对应用行为的影响很大。共有三种模式:

  • AAUDIO_PERFORMANCE_MODE_NONE 是默认模式。这种模式使用在延迟时间与节能之间取得平衡的基本流。
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 使用较小的缓冲区和经优化的数据路径,以减少延迟时间。
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING 使用较大的内部缓冲区,以及以延迟时间为代价换取节能优势的数据路径。

您可以通过调用 setPerformanceMode() 来选择性能模式,并通过调用 getPerformanceMode() 来发现当前模式。

如果在您的应用中缩短延迟时间比节能更重要,请使用 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY。这对交互性非常强的应用(例如游戏或键盘合成器)非常有用。

如果在您的应用中节能比缩短延迟时间更重要,请使用 AAUDIO_PERFORMANCE_MODE_POWER_SAVING。这在回放先前生成的音乐的应用(例如流式音频或 MIDI 文件播放器)中很常见。

在当前版本的 AAudio 中,为了尽量减少延迟时间,您必须将 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 性能模式与高优先级回调配合使用。请参阅以下示例:

// Create a stream builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);

// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);

线程安全

线程安全而言,AAudio API 并非完全安全。部分 AAudio 函数不能同时从多个线程中并行调用。这是因为 AAudio 避免使用互斥量,而互斥量可能会导致线程抢占和干扰。

为确保安全,请勿从两个不同线程中调用 AAudioStream_waitForStateChange() 或者读写同一个流。同样地,请勿在一个线程中关闭流的同时在另一个线程中读写流。

返回流设置的调用(例如,AAudioStream_getSampleRate()AAudioStream_getChannelCount())线程安全。

下列调用线程也属于安全线程:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*()AAudioStream_getTimestamp() 除外)

已知问题

  • 因为 Android O DP2 版本不使用快速音轨,因此阻塞 write() 的音频延迟时间较长。您可使用回调来减少延迟时间。

其他资源

如需了解详情,请参考以下资源:

API 参考文档

Codelab

视频