Android-Native开发系列之利用AAudio播放音频

Android-Native开发系列之利用AAudio播放音频

谈到在Android C/C++层实现音频播放/录制功能的时候,大家可能首先会想到的是利用opensles去做,这确实是一直不错的实现方式,久经考验,并且适配比较广。

但如果你的项目最低版本支持Android 26及以上的话,且想追求最小的延迟,最高的性能。那可以考虑一下AAudio。

博主之前在项目中使用opensles处理音频,后来又分别尝试过利用oboe,aaudio实现音频处理,小有体会,便记录一下,方便自己与他人。

什么是AAudio?

AAudio是在Android 8.0时提出的一种新型的C风格接口的Android底层音频库,它的设计目标是追求更高的性能与低延迟。aaudio的设计很单纯,就是播放和录制音频原始数据,拿播放来说的话,就只能播放pcm数据。它不像opensles,opensles可以播放mp3,wav等编码封装后的音频资源。也就是说它不包含编解码模块。

这里简单提一嘴oboe,oboe是对opensles和aaudio的封装。它内部有自己的一个对设备判断逻辑,来选择内部是用aaudio引擎播放声音还是用opensles引擎播放声音。比如在低于Android 8.0的设备,它会选择用opensles播放。

配置AAudio开发环境

添加aaudio头文件

#include <aaudio/AAudio.h>

CMakeLists.txt 链接aaudio

target_link_libraries( # Specifies the target library.
        aaudiodemo
        # Links the target library to the log library
        # included in the NDK.
        ${log-lib}
        android
        aaudio)

AAudioStream

AudioStream 在AAudio中是一个很重要的概念,它的结构体是:AAudioStream。与AAudio交换音频数据实现录音和播放效果的实质上是与AAudio中的AAudioStream交换数据。

我们在使用AAudio播放音频的时候,首先要做的就是创建AAudioStream。

1.创建AAudioStreamBuilder

AAudioStream的创建设计成了builder模式,所以我们需要先创建一个相应的builder对象。

AAudioStreamBuilder *builder = nullptr;
aaudio_result_t result = AAudio_createStreamBuilder(&builder);
if (result != AAUDIO_OK) {
  LOGE("AAudioEngine::createStreamBuilder Fail: %s", AAudio_convertResultToText(result));

2.配置AAudioStream

通过builder的setXXX函数来配置AAudioStream。

AAudioStreamBuilder_setDeviceId(builder, AAUDIO_UNSPECIFIED);
AAudioStreamBuilder_setFormat(builder, mFormat);
AAudioStreamBuilder_setChannelCount(builder, mChannel);
AAudioStreamBuilder_setSampleRate(builder, mSampleRate);
// We request EXCLUSIVE mode since this will give us the lowest possible latency.
// If EXCLUSIVE mode isn't available the builder will fall back to SHARED mode.
AAudioStreamBuilder_setSharingMode(builder, AAUDIO_SHARING_MODE_EXCLUSIVE);
AAudioStreamBuilder_setPerformanceMode(builder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
AAudioStreamBuilder_setDirection(builder, AAUDIO_DIRECTION_OUTPUT);
// AAudioStreamBuilder_setDataCallback(builder, aaudiodemo::dataCallback, this);
// AAudioStreamBuilder_setErrorCallback(builder, aaudiodemo::errorCallback, this);

简述一下上面的函数,具体大家可以看源码里面的注释。

DeviceId----指定物理音频设备,例如内建扬声器,麦克风,有线耳机等等。这里AAUDIO_UNSPECIFIED表示有AAudio根据上下文自行决定,如果是播放音频的话,一般它会选择扬声器。

format,channel,sample就不细说了,很容易理解,这里提一嘴的是,测试下来发现AAudio只支持单声道和双声道音频,5.1、7.1这种多声道音频,不支持播放。opensles也是,虽然提供了多声道的枚举值,但是当真正设置进去的时候,会提示说不支持。

SharingMode 分为:AAUDIO_SHARING_MODE_EXCLUSIVEAAUDIO_SHARING_MODE_SHARED ,因为AAudioStream是要和device绑定的,独占模式就是独占这个audio device,别的audio stream不能访问,独占模式下延迟会更小,但是要注意不用的时候及时关闭释放,不然别的流没法访问该audio device了。共享模式就是多个音频流可以共享一个audio device,官方说共享模式下,同一个audio device所有音频流可以实现混音,这个混音我还没试,后面抽空试试。到时候会在文末补充结论。

PerformanceMode:

  • AAUDIO_PERFORMANCE_MODE_NONE 默认模式,在延迟和省电间,自己平衡
  • AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 更注重延迟
  • AAUDIO_PERFORMANCE_MODE_POWER_SAVING 更注重省电
  • Direction

  • AAUDIO_DIRECTION_INPUT 录音的时候用
  • AAUDIO_DIRECTION_OUTPUT 播放的时候用
  • 最后两行被注释的回调,我们后面再说。

    3.创建AAudioStream

    aaudio_result_t  AAudioStreamBuilder_openStream(AAudioStreamBuilder* builder,
            AAudioStream** stream)
    

    通过openStream函数,获取到指定配置的AAudioStream对象,接着就可以拿着audio stream处理音频数据了。这里在成功创建玩AAudioStream之后,可以通过调用AAudioStream的相关getXXX函数,获取“真正” audio stream配置,之前通过audio stream builder设置的配置是我们的意向配置,一般来说,意向配置就是最终配置,但是也会存在偏差,所以在调式阶段,最好再打印一下,真正的audio stream配置,帮助开发者获取确切信息。

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

    在创建完成AAudioStream后,需要释放AAudioStreamBuilder对象。

    AAudioStreamBuilder_delete(builder);
    

    4.操作AAudioStream

    AAudioStream 的生命周期

  • Started
  • Paused
  • Flushed
  • Stopped
  • Disconnected
  • Error
  • 我们先说前五种状态,它们的状态变化可以用下面的流程图表示,虚线框表示瞬时状态,实线框表示稳定状态:

    其中涉及的函数有:

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

    上面的这些函数是异步调用,不会阻塞。也就是,调用完函数后,audio stream的状态不会立马转移到指定状态。它会先转移到相应的瞬时状态,看上面的流程图就能知道,相应的瞬时状态有如下几种:

  • Starting
  • Pausing
  • Flushing
  • Stopping
  • Closing
  • 那调用完requestXXX函数后,怎么知道状态真正切换到相应的状态呢?一般来说,我们没必要知道这个信息,所以AAudio对于这方面支持一般,都没提供什么接口。如果你有这方面的需求的话,那么可以看看,下面的函数,可以勉强符合你的需求。

    aaudio_result_t AAudioStream_waitForStateChange(AAudioStream* stream,
            aaudio_stream_state_t inputState, aaudio_stream_state_t *nextState,
            int64_t timeoutNanoseconds)
    

    这个函数需要注意的是inputStatenextState参数,inputState参数代表当前的状态,可以通过AAudioStream_getState获取,nextState是指状态发生变化后,新的状态值,这里新的状态值是不确定的,但有一点确定的是,一定跟inputState值不一样

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

    例如上面的代码,waitForStateChange函数调用后,nextState就一定是Paused吗?不一定,有可能是其他的状态,比如:Disconnected,但是nextState一定不等于inputState。

  • 不要在调用AAudioStream_close之后,调用waitForStateChange函数
  • 当其他线程运行waitForStateChange函数时,不要调用AAudioStream_close函数
  • 5.AAudioStream处理音频数据

    当audio stream启动后,有两种方式来处理音频数据。

  • 通过AAudioStream_writeAAudioStream_read函数向流里写数据和读数据,使用此方式需要自己创建线程控制数据读写。
  • 通过callback的方式,使用此方式,会更高效,延迟更低,是官方推荐的方式
  • 通过write、read函数直接读写数据

    先看下函数原型:

    aaudio_result_t AAudioStream_write(AAudioStream* stream,
                                   const void *buffer,
                                   int32_t numFrames,
                                   int64_t timeoutNanoseconds)
    

    buffer: 音频原始数据

    numFrames:请求处理的帧数,例如:16位双声道的数据,那么该值就是:bufferSize/(16/8)/2

    timeoutNanoseconds: 最长阻塞时间,当值为0时,表示不阻塞

    return value: 表示实际处理的帧数

    知道函数的使用方式后,我们就可以在愉快的往里面填充数据了~

    通过callback回调的方式处理数据

    为什么官方说推荐使用callback方式呢,主要原因我认为有几点:

  • 使用callback方式,aaudio内部会通过一个高优先级的优化后的专属线程处理回调,会避免因线程抢占等问题出现杂音。
  • 使用callback方式,延迟更低。
  • 使用直接向流里读写数据,需要自己维护一个播放线程,成本高,且有bug风险。而且如果要创建多个音频播放器,考虑出现多个线程,进而出现资源紧张的问题。
  • 那这么说,是不是就一定得用callback方式了呢,也不是,经过作者的测试发现,关于延迟的指标,除非对延迟要求很高的产品,大多数情况下,使用直接读写数据到流的方式也是没问题的。所以选择具体方案还是要根据项目的真实情况决定。

    具体怎么通过callback的方式处理数据呢?

    还记得在配置AAudioStream这一节的时候,被注释的两行代码嘛。

    // AAudioStreamBuilder_setDataCallback(builder, aaudiodemo::dataCallback, this); // AAudioStreamBuilder_setErrorCallback(builder, aaudiodemo::errorCallback, this);

    当使用callback模式处理音频数据的时候,就需要设置这两个函数。

    dataCallback 在AAudio需要数据时触发,我们只需要往里面写入指定大小的数据即可。

    typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)(
            AAudioStream *stream,
            ///上下文环境
            void *userData,
            ///填充音频数据
            void *audioData,
            ///需要填充多少帧数据,具体的换算方式:dataSize = numFrames*channels*(format == AAUDIO_FORMAT_PCM_I16 ? 2 : 1)
            int32_t numFrames);
    

    该回调的返回值有两种:

  • AAUDIO_CALLBACK_RESULT_CONTINUE 表示继续播放
  • AAUDIO_CALLBACK_RESULT_STOP 表示停止播放,该回调不会再触发
  • 因为dataCallback会频繁调用。所以最好不要在此回调中做一下耗时的,很重的任务。

    errorCallback 当出现错误发生的时候或者断开连接的时候,此回调会被触发。常见的一个例子是:如果audio device disconnected时,会触发errorCallback,这个时候需要新开一个线程,重新创建AAudioStream。

    typedef void (*AAudioStream_errorCallback)(
            AAudioStream *stream,
            void *userData,
            aaudio_result_t error);
    

    在此回调中,下列函数不要直接调用,需要新开一个线程处理

    AAudioStream_requestStop()
    AAudioStream_requestPause()
    AAudioStream_close()
    AAudioStream_waitForStateChange()
    AAudioStream_read()
    AAudioStream_write()
    

    AAudioStream相关的getXXX函数可以直接调用,如:AAudioStream_get*()**

    关于AAudio的使用demo,已上传至github,觉得不错的话,就给个star吧~ღ( ´・ᴗ・` )比心

    [AAUDIODEMO​]  github.com/MRYangY/AAu…

    6.销毁AAudioStream

    AAudioStream_close(stream);
    

    Extra Info

    上面的章节属于必须内容,下面的为补充内容,按需了解。

    underrun & overrun

    underrun和overrun是音频数据的生产与消费节奏不匹配导致的。

    underrun 是指在播放音频的时候,没有及时往audio stream写入的数据,系统没有可用的音频数据。

    overrun 是指在录制音频的时候,没有及时的从audio stream读取数据,导致音频没人接收,就给丢了。

    这两种情况都会导致音频出现问题。

    AAudio是怎么解决这种问题呢?

    利用动态调整缓冲区大小来降低延迟,避免underrun。涉及到的函数有:

    ///app一次处理音频的数据量-(帧数),返回的值是经过系统优化后的适应低延迟的值,可作为出现XRun时的StepSize
    int32_t AAudioStream_getFramesPerBurst(AAudioStream* stream);
    ///缓冲区大小设置,实现低延迟的,解决xrun的本质就是动态的调节这个size的大小
    aaudio_result_t AAudioStream_setBufferSizeInFrames(AAudioStream* stream,int32_t numFrames);
    int32_t AAudioStream_getBufferSizeInFrames(AAudioStream* stream)
    ///通过该方法,可以知道是否发生underrun或者overrun,进而决定该如何调整缓冲区大小
    int32_t AAudioStream_getXRunCount(AAudioStream* stream)
    

    演示调用流程:

    int32_t previousUnderrunCount = 0;
    int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);
    ///通常在最开始的时候把framesPerBurst的值通过AAudioStream_setBufferSize设置给bufferSize,让它两一样会更容易达到低延迟
    int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);
    int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);
    while (run) {
        /// 向AAudioStream写数据
        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);
    

    上面的代码很容易理解,就是刚开始会初始化一块小的buffer, 当发生underrun的时候,根据framesPerBurst不断的增大buffer,来实现低延迟。

    Thread safety

    AAudio的接口不是完全线程安全的。在使用的时候需要注意:

  • 不要在多个线程并发调用AAudioStream_waitForStateChange()/read/write函数。
  • 不要在一个线程关闭流,另一个线程读写流。
  • 线程安全的有:

  • AAudio_convert*ToText()
  • AAudio_createStreamBuilder()
  • AAudioStream_get*() 系列函数,除了 AAudioStream_getTimestamp()
  • aaudio接口很简单,跟opensles的代码量相比,少多了。不过功能比opensles少一些。像是解码,控制音量等,aaudio都木有。大家看自己需求选择吧。

    给个demo工程链接,配合着文章看看就懂了。

    github.com/MRYangY/AAu…

    developer.android.com/ndk/guides/…