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开发环境

  1. 添加aaudio头文件

    #include <aaudio/AAudio.h>
    
  1. 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 的生命周期

    • Open
    • Started
    • Paused
    • Flushed
    • Stopped
    • Disconnected
    • Error

我们先说前五种情况,它们的情况变化可以用下面的流程图标明,虚线框标明瞬时情况,实线框标明安稳情况:

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

其间触及的函数有:

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方法呢,主要原因我认为有几点:

  1. 运用callback方法,aaudio内部会经过一个高优先级的优化后的专属线程处理回调,会防止因线程抢占等问题呈现杂音。
  2. 运用callback方法,推延更低。
  3. 运用直接向流里读写数据,需求自己保护一个播放线程,本钱高,且有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/…

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。