AAudio

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

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

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

音频流

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 float -1.0 至 +1.0

AAudio 可以独立执行样本转换。 例如,如果应用写入 FLOAT 数据,但 HAL 使用 PCM_I16,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):

  • Open
  • Started
  • Paused
  • Flushed
  • Stopped

仅当流处于 Started 状态时,数据才会通过流来流动。 要转换流所处的状态,请使用以下其中一个请求转换状态的函数:

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

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

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

  • Starting
  • Pausing
  • Flushing
  • Stopping
  • Closing

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

AAudio 生命周期

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

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

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

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);

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

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

读取和写入音频流

流开始后,您可使用 AAudioStream_read(stream, buffer, numFrames, timeoutNanos)AAudioStream_write(stream, buffer, numFrames, timeoutNanos) 函数对其进行读写。

对于传输指定帧数的阻塞读取或写入操作,请将 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 函数配合使用。

断开连接的音频流

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

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

流断开连接后,其状态为“Disconnected”,任何尝试执行 write() 或其他函数的操作都会返回 AAUDIO_ERROR_DISCONNECTED。 流断开连接后,您只能将其关闭。

如果您需要在音频设备断开连接时接收通知,请编写 AAudioStream_errorCallback 函数,然后使用 AAudioStreamBuilder_setErrorCallback() 注册该函数。

回调应检查流的状态,如以下示例所示。 您不应从回调中关闭或重新打开流,而应使用另一个线程。 请注意,如果打开新的流,其特征可能与原始流不同(例如,framesPerBurst):

void errorCallback(AAudioStream *stream,
                   void *userData,
                   aaudio_result_t error){

  aaudio_stream_state_t streamState = AAudioStream_getState(stream);
  if (streamState == AAUDIO_STREAM_STATE_DISCONNECTED){
    // Handle stream disconnect on a separate thread
    ...
  }
}

优化性能

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

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

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

注:请勿将 AAudio 的内部缓冲区与 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() 除外)

注:当流使用回调函数时,从回调线程中执行读/写,同时从运行流的线程中关闭流并无安全问题。

代码示例

我们的 GitHub 页面上提供两个小型 AAudio 演示应用:

  • Hello-Audio 生成正弦波并回放音频。
  • Echo 演示如何实现输入/输出往返音频循环。 它使用对两个流进行同步的回调函数,并连续执行缓冲区调整。

如需了解使用 OpenSL ES 来尽量减少输出延迟时间并避免音频干扰的详细信息,请参阅简单合成

已知问题

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