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_EXCLUSIVE 和 AAUDIO_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
我们先说前五种情况,它们的情况变化可以用下面的流程图标明,虚线框标明瞬时情况,实线框标明安稳情况:

其间触及的函数有:
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)
这个函数需求留心的是inputState 和 nextState参数,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_write和AAudioStream_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/…