文章目录

  • 1. 前言
  • 2. 工程准备
  • 3. 低延迟音频原理及功能实现方案
  • 4. 使用OpenSL ES
    • 4.1 播放器实现
    • 4.2 录音器实现
    • 4.3 Echo实现
  • 5. 使用AAudio
    • 5.1 播放器实现
    • 5.2 录音器实现
    • 5.3 Echo实现
  • 6. 使用Oboe
  • 7. 功能配置

1. 前言

Android提供了很多的多媒体接口,通常在java层,我们常用的就是AudioTrack和MediaPlayer进行音频播放。MediaPlayer不光可以播放音频,也可以播放视频,并支持少部分的解码。

而由于音视频通常计算量都很大,所以很多音视频方面的工作都会放在native层进行。Android在native层同样提供了一些组件来进行音频的播放和录制:

  • OpenSL ES:这是Android从很早就开始支持的,它类似于OpenGL ES,所不同的是它完全服务于音频。可以说现在所有的Android手机都会支持。兼容性最好。它无法指定录制或者播放设备。
  • AAudio:这是谷歌在Android 8.0后引入的,它支持一些简单的解码、音频录制和播放以及一些效果等。官方网页为AAudio-Google。它可以指定录制或播放设备。
  • Oboe:这个并不是新的音频框架,而只是为了方便使用OpenSL ES和AAudio,对两者进行的封装,隐藏了大部分琐碎的细节,并且其api也是基于c++风格的,更加方便。日常使用基本只依赖这个即可。

native部分的音频框架相对于java部分的音频框架来说,性能是更高的,所以如果你希望降低app的音频通路延迟(游戏等),基本上只能选择native。

本篇文章会分别使用三者,演示如何构建录制器和播放器,并实现低延迟的echo功能。

2. 工程准备

工程源码放在我的github上:FastPathAudioEcho

新建一个工程,并使其支持c++。

对于OpenSL ES和AAudio,这些库在系统中是被包含的,因此只要在CMakeLists中链接即可:

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

至于oboe,它并未被包含在Android SDK中,因此需要到github上搜索,然后使用仓库或下载源码进行配置。地址是:Oboe-Google。编译方法也可以从这找到。
由于使用仓库对构建工具版本有要求,因此我选择的是直接下载源码进行编译。下面是我的配置:

CMakeLists.txt,位置在源码的cpp文件夹中。


cmake_minimum_required(VERSION 3.4.1)file(GLOB CPP_FILES "./*.cpp", "./SLESEcho/*.cpp", "./AAudioEcho/*.cpp", "./OboeEcho/*.cpp")include_directories("./", "./SLESEcho/", "./AAudioEcho/", "./OboeEcho/")add_library( # Sets the name of the library.native-lib# Sets the library as a shared library.SHARED# Provides a relative path to your source file(s).${CPP_FILES})# Set the path to the Oboe directory.set (OBOE_DIR "D:\\workspace\\AndroidProject\\oboe")# Add the Oboe library as a subdirectory in your project.
# add_subdirectory tells CMake to look in this directory to
# compile oboe source files using oboe's CMake file.
# ./oboe specifies where the compiled binaries will be storedadd_subdirectory (${OBOE_DIR} ./oboe)# Specify the path to the Oboe header files.
# This allows targets compiled with this CMake (application code)
# to see public Oboe headers, in order to access its API.include_directories (${OBOE_DIR}/include)# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.find_library( # Sets the name of the path variable.log-lib# Specifies the name of the NDK library that# you want CMake to locate.log)# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.target_link_libraries( # Specifies the target library.native-libOpenSLESaaudiooboe# Links the target library to the log library# included in the NDK.${log-lib})

然后,指定一些常量和结构体放在一个头文件中,方便使用。

// Constants.h
#ifndef FASTPATHAUDIOECHO_CONSTANTS_H
#define FASTPATHAUDIOECHO_CONSTANTS_H#include <iostream>
#include <stdlib.h>
#include <string.h>#define NANO_SEC_IN_MILL_SEC 100000
struct AudioFrame{int64_t pts;int16_t *data;int32_t sampleCount;int32_t maxDataSizeInByte = 0;AudioFrame(int32_t dataLenInByte){this->maxDataSizeInByte = dataLenInByte;pts = 0;sampleCount = 0;data = (int16_t *)malloc(maxDataSizeInByte);memset(data, 0, maxDataSizeInByte);}~AudioFrame(){if(data != NULL){free(data);}}
};
#endif //FASTPATHAUDIOECHO_CONSTANTS_H

3. 低延迟音频原理及功能实现方案

官方文档分别在OpenSL ES和AAudio页面介绍了如何启用低延迟音频。总结起来共有以下四点:

  • 获取硬件最佳采样率
  • 获取硬件最佳buffer长度
  • 对框架启用低延迟模式
  • 使用回调函数而非客户端主动读取或写入数据

其中,最佳采样率和最佳buffer长度是针对硬件的。每个手机厂商的每款机型,可能由于硬件芯片的不同,芯片的原生采样率和音频buffer长度都不同,因此这两个数据需要程序运行时动态查询。使用硬件原生采样率和buffer长度的目的,就是避免系统中对音频数据进行的重采样或帧缓冲等操作。
而启用低延迟模式可以理解为,不要让系统在音频中添加效果。并且提高音频线程的优先级。

查询最佳采样率和buffer长度可以通过AudioManager进行:

val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
val sampleRate = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE).toInt()
val framesPerBuffer = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER).toInt()

这个工程三种API的实现思路是一样的。首先,实现一个线程安全的数据结构,方便从recorder到player传输数据。然后依据每种api实现对应的recorder和player。Echo则是对回放功能的实现类,它使用了player和recorder,并管理两者之间的数据传输,以及播放状态。

因为练手的原因,我一共实现了两种线程安全的数据结构:BlockQueueBlockRingBuffer。前者基于一个list实现,后者是基于数组实现的环形buffer。

两者的基本特性是一样的:线程安全,内部空时get操作会被阻塞,内部满时put操作会被阻塞。

为了尽可能降低延迟,在启动时都是先启动player,这个时候由于buffer是空的,所以player会被阻塞,然后启动recorder,一旦recorder将数据放到buffer里,那player就能立即开始播放。

4. 使用OpenSL ES

4.1 播放器实现

源码文件是SLESPlayer。

对于OpenSL ES来说,尽管官方文档中说明了它的输出流也可以设置低延迟模式SL_ANDROID_PERFORMANCE_LATENCY,但是我在一加手机上无法配置成功。在官方的android-echo示例中也并没有找到相关的配置代码。

这里贴一下建立播放器的源码,因为OpenSL ES用起来还是挺复杂的,一是文档不是很全,二是源码没有注释。

/*** 初始化播放器。* engine:OpenSLES引擎。* dataCallback:数据回调接口。为空时,需要客户端自行向播放器填充数据。* sampleRate:采样率* channelCount:声道数* framesPerBuffer:一个buffer包含多少帧,通常这是在使用低延迟音频时会设置。* return:是否成功初始化。* */
bool SLESPlayer::init(SLESEngine &engine, ISLESPlayerCallback *dataCallback, int32_t sampleRate, int32_t channelCount,int32_t framesPerBuffer) {this->dataCallback = dataCallback;this->sampleRate = sampleRate;this->framesPerBuffer = framesPerBuffer;this->channelCount = channelCount;// 初始化一个buffer。if(audioBuffer){free(audioBuffer);}audioBuffer = (int16_t *)calloc(framesPerBuffer * channelCount, sizeof(int16_t));SLEngineItf engineEngine = engine.getEngine();SLresult result;// 初始化一个outputMixSLInterfaceID ids1[1] = {SL_IID_OUTPUTMIX};SLboolean reqs1[1] = {SL_BOOLEAN_FALSE};result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 0, ids1, reqs1);if(result != SL_RESULT_SUCCESS){return false;}result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);if(result != SL_RESULT_SUCCESS){return false;}// Create playerSLDataLocator_AndroidSimpleBufferQueue bufferQueue = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};SLDataFormat_PCM pcmFormat = {SL_DATAFORMAT_PCM, (uint32_t)channelCount, (uint32_t)sampleRate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16,SL_SPEAKER_FRONT_CENTER, SL_BYTEORDER_LITTLEENDIAN};SLDataSource audioSrc = {&bufferQueue, &pcmFormat};SLDataLocator_OutputMix locOutputMix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};SLDataSink audioSink = {&locOutputMix, NULL};SLInterfaceID ids2[2] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME};SLboolean reqs2[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE};result = (*engineEngine)->CreateAudioPlayer(engineEngine, &playerObject, &audioSrc, &audioSink, 2, ids2, reqs2);if(result != SL_RESULT_SUCCESS){return false;}result = (*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);if(result != SL_RESULT_SUCCESS){return false;}// 这是配置低延迟模式的代码,但是configItf一直是null。系统log打印W/libOpenSLES: Leaving Object::GetInterface (SL_RESULT_FEATURE_UNSUPPORTED)SLAndroidConfigurationItf configItf = nullptr;result = (*playerObject)->GetInterface(playerObject, SL_IID_ANDROIDCONFIGURATION, &configItf);if(result == SL_RESULT_SUCCESS && configItf != nullptr){// Set the performance mode.SLuint32 performanceMode = SL_ANDROID_PERFORMANCE_LATENCY;result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE,&performanceMode, sizeof(performanceMode));if(result != SL_RESULT_SUCCESS){LOGE("failed to enable low latency of player");}} else{LOGE("failed to get config obj");}result = (*playerObject)->GetInterface(playerObject, SL_IID_PLAY, &playerPlay);if(result != SL_RESULT_SUCCESS){return false;}result = (*playerObject)->GetInterface(playerObject, SL_IID_BUFFERQUEUE, &playerBufferQueue);if(result != SL_RESULT_SUCCESS){return false;}if(dataCallback){result = (*playerBufferQueue)->RegisterCallback(playerBufferQueue, playerCallback, this);if(result != SL_RESULT_SUCCESS){return false;}}// 要注意,OpenSLES在创建好播放器或者录音器后,需要手动Enqueue一次,才能触发主动回调。result = (*playerBufferQueue)->Enqueue(playerBufferQueue, audioBuffer, framesPerBuffer * channelCount * sizeof(int16_t));if(result != SL_RESULT_SUCCESS){return false;}(*playerPlay)->SetPlayState(playerPlay, SL_PLAYSTATE_STOPPED);return true;
}

这里的采样率和buffer长度就要设置为之前通过AudioManager查询得到的数据。

特别注意的是,OpenSL ES每次在start时需要手动Enqueue一次空buffer,这样它才会主动回调给它设置的callback。否则,不光是可能不会回调,还有可能出现杂音等一系列问题。

4.2 录音器实现

源码为SLESRecorder

对于recorder来说,设置recorder的config为SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION,是降低录音延迟的设置。

 /*** 初始化。* engine:引擎* dataCallback:输出录音数据的回调。为空时,需要客户端主动从录音器读取数据* sampleRate:采样率* framesPerBuffer:buffer可以容纳多少帧数据。对于录音器来说,该选项并不会影响延迟。录音器总是以尽可能快的方式进行。* return:是否成功初始化* */
bool SLESRecorder::init(SLESEngine &engine, ISLESRecorderCallback *dataCallback, int32_t sampleRate, int32_t framesPerBuffer) {this->dataCallback = dataCallback;this->sampleRate = sampleRate;this->framesPerBuffer = framesPerBuffer;if(audioBuffer){free(audioBuffer);}audioBuffer = (int16_t *)calloc(framesPerBuffer, sizeof(int16_t));const SLEngineItf engineEngine = engine.getEngine();if(!engineEngine){LOGE("engineEngine null");return false;}SLresult result;SLDataLocator_IODevice deviceInputLocator = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL };SLDataSource inputSource = { &deviceInputLocator, NULL };SLDataLocator_AndroidSimpleBufferQueue inputLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2 };SLDataFormat_PCM inputFormat = { SL_DATAFORMAT_PCM, 1, (SLuint32)sampleRate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT, SL_BYTEORDER_LITTLEENDIAN };SLDataSink inputSink = { &inputLocator, &inputFormat };const SLInterfaceID inputInterfaces[2] = { SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION };const SLboolean requireds[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE };// 创建AudioRecorderresult = (*engineEngine)->CreateAudioRecorder(engineEngine, &recorderObject, &inputSource, &inputSink, 2, inputInterfaces, requireds);if(result != SL_RESULT_SUCCESS){LOGE("create recorder error");return false;}// 设置recorder的config为SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION,这是开启录音器低延迟的方法。SLAndroidConfigurationItf recordConfig;result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDCONFIGURATION, &recordConfig);if(result == SL_RESULT_SUCCESS){SLuint32 presentValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION;(*recordConfig)->SetConfiguration(recordConfig, SL_ANDROID_KEY_RECORDING_PRESET, &presentValue, sizeof(SLuint32));}// 初始化AudioRecorderresult = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);if(result != SL_RESULT_SUCCESS){LOGE("realise recorder object error");return false;}// 获取录制器接口result = (*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);if(result != SL_RESULT_SUCCESS){LOGE("get interface error");return false;}// 获取音频输入的BufferQueue接口result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorderBufferQueue);if(result != SL_RESULT_SUCCESS){LOGE("get buffer queue error");return false;}if(dataCallback){result = (*recorderBufferQueue)->RegisterCallback(recorderBufferQueue, recorderCallback, this);if(result != SL_RESULT_SUCCESS){LOGE("register callback error");return false;}}// 要注意,OpenSLES在创建好播放器或者录音器后,需要手动Enqueue一次,才能触发主动回调。result = (*recorderBufferQueue)->Enqueue(recorderBufferQueue, audioBuffer, framesPerBuffer * sizeof(int16_t));if(result != SL_RESULT_SUCCESS){return false;}(*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_STOPPED);return true;
}

4.3 Echo实现

源码是SLESEcho

它使用上面的player和recorder,因此这部分的代码已经相当简单,注意启动时的顺序,以及结束时的防止死锁:

void SLESEcho::start() {// 为了降低延迟,先启动播放器,让它的回调函数阻塞,一旦录音器有数据填充进来,可以立刻开始播放。player.start();recorder.start();
}void SLESEcho::stop() {// 首先调用对应部件的stop方法,该方法是异步的,不会阻塞。但是回调函数可能仍然在阻塞,因此要对buffer进行设置,// 解除等待状态,然后再恢复阻塞功能。recorder.stop();buffer.setWaitPutState(false);player.stop();buffer.setWaitGetState(false);buffer.setWaitGetState(true);buffer.setWaitPutState(true);
}

5. 使用AAudio

使用AAudio的回调函数模式时要注意:AAudio回调函数需要返回AAUDIO_CALLBACK_RESULT_CONTINUEAAUDIO_CALLBACK_RESULT_STOP,来指示流是否继续进行。但是我发现如果你返回了stop,那么之后再调用start时,回调函数并不会被调用,也就无法正常录制或播放。而且AAudio的回调函数中并没有任何方式可以告诉AAudio你从它的audioData这个buffer里读取或写入了多少帧数据,所以它默认应该是你将buffer全都写满或者全都读取了。因此我建议,在录音端并不需要做任何特殊处理,但是在播放端,回调函数应该一直返回AAUDIO_CALLBACK_RESULT_CONTINUE,如果你因为播放功能已经停止,或者其他什么原因,导致你无法给到AAudio播放需要的那么多数据,只要简单将传到回调函数的audioData这个buffer全部置0,再尽可能写入即可,这样表现出的只是静音而已,并不会出现异常情况。对于流的状态,只需要通过流本身的start或stop进行即可。

5.1 播放器实现

源码是AAudioPlayer

相比于OpenSL ES,AAudio则要清楚很多。依旧只贴一下创建的代码。AAudio的输出流可以设置低延迟模式,为AAUDIO_PERFORMANCE_MODE_LOW_LATENCY

bool AAudioPlayer::init(IAAudioPlayerCallback *dataCallback, int32_t sampleRate, int32_t channelCount, PERFORMANCE_MODE mode, int32_t framesPerBuffer) {this->dataCallback = dataCallback;this->sampleRate = sampleRate;this->channelCount = channelCount;this->framesPerBuffer = framesPerBuffer;emptyBuffer = (int16_t *)calloc(framesPerBuffer * channelCount, sizeof(int16_t));aaudio_result_t result;AAudioStreamBuilder *outputBuilder;result = AAudio_createStreamBuilder(&outputBuilder);if(result != AAUDIO_OK){LOGE("create output stream builder error");AAudioStreamBuilder_delete(outputBuilder);return false;}AAudioStreamBuilder_setDirection(outputBuilder, AAUDIO_DIRECTION_OUTPUT);AAudioStreamBuilder_setFormat(outputBuilder, AAUDIO_FORMAT_PCM_I16);AAudioStreamBuilder_setSamplesPerFrame(outputBuilder, framesPerBuffer);AAudioStreamBuilder_setSampleRate(outputBuilder, sampleRate);AAudioStreamBuilder_setChannelCount(outputBuilder, channelCount);if(dataCallback){AAudioStreamBuilder_setDataCallback(outputBuilder, output_callback, this);}AAudioStreamBuilder_setPerformanceMode(outputBuilder, mode); // 在这里设置低延迟模式result = AAudioStreamBuilder_openStream(outputBuilder, &outputStream);AAudioStreamBuilder_delete(outputBuilder);if(result != AAUDIO_OK){LOGE("open play stream failed");return false;}return true;
}

5.2 录音器实现

源码是AAudioRecorder

同输出流一样,AAudio的输入流可以设置低延迟模式,为AAUDIO_PERFORMANCE_MODE_LOW_LATENCY

bool AAudioRecorder::init(IAAudioRecorderCallback *dataCallback, int32_t sampleRate, PERFORMANCE_MODE mode, int32_t framesPerBuffer, int32_t micID) {this->sampleRate = sampleRate;this->framesPerBuffer = framesPerBuffer;this->micID = micID;this->dataCallback = dataCallback;this->mode = mode;aaudio_result_t result;AAudioStreamBuilder *inputBuilder;result = AAudio_createStreamBuilder(&inputBuilder);if(result != AAUDIO_OK){LOGE("create input stream builder error");return false;}if(micID != -1){AAudioStreamBuilder_setDeviceId(inputBuilder, micID);}AAudioStreamBuilder_setFormat(inputBuilder, AAUDIO_FORMAT_PCM_I16);AAudioStreamBuilder_setSamplesPerFrame(inputBuilder, framesPerBuffer);AAudioStreamBuilder_setSampleRate(inputBuilder, sampleRate);AAudioStreamBuilder_setChannelCount(inputBuilder, 1);AAudioStreamBuilder_setDirection(inputBuilder, AAUDIO_DIRECTION_INPUT);AAudioStreamBuilder_setPerformanceMode(inputBuilder, mode); // 在这里设置低延迟模式。if(dataCallback){AAudioStreamBuilder_setDataCallback(inputBuilder, input_callback, this);}result = AAudioStreamBuilder_openStream(inputBuilder, &inputStream);if(result != AAUDIO_OK){LOGE("open record stream failed");return false;}aaudio_performance_mode_t actualPerformance = AAudioStream_getPerformanceMode(inputStream);LOGD("actual performance mode is %d", actualPerformance);return true;}

5.3 Echo实现

源码为AAudioEcho

Echo的实现基本同OpenSL ES的一致,就不贴了。

6. 使用Oboe

Oboe使用起来就更简单了,按照文档配置好编译即可。由于Oboe已经对它的播放和录制模块封装得非常好,因此我就不再单独封装出player和recorder。直接贴出Echo的初始化。

源码是OboeEcho

    /*** 初始化* sampleRate:采样率* api:指定使用OpenSLES或者是AAudio。不设置时由系统自行确定。* framesPerBuffer:每个buffer包含多少帧。* micID:仅当Oboe使用AAudio进行echo时才有效。* */
bool OboeEcho::init(int32_t sampleRate, AudioApi api, int32_t framesPerBuffer, int32_t micID) {this->sampleRate = sampleRate;this->framesPerBuffer = framesPerBuffer;this->micID = micID;buffer = new BlockRingBuffer<int16_t>(3 * framesPerBuffer);oboe::Result result;AudioStreamBuilder inputBuilder;inputBuilder.setAudioApi(api);inputBuilder.setCallback(this);inputBuilder.setDirection(Direction::Input);inputBuilder.setChannelCount(ChannelCount::Mono);inputBuilder.setPerformanceMode(PerformanceMode::LowLatency); // 指定为低延迟inputBuilder.setSharingMode(SharingMode::Shared);inputBuilder.setFormat(AudioFormat::I16);inputBuilder.setSampleRate(sampleRate);inputBuilder.setFramesPerCallback(framesPerBuffer);inputBuilder.setDeviceId(micID);result = inputBuilder.openStream(&recordStream);if(result != Result::OK){LOGE("create input stream failed");return false;}AudioStreamBuilder outputBuilder;outputBuilder.setAudioApi(api);outputBuilder.setCallback(this);outputBuilder.setDirection(Direction::Output);outputBuilder.setChannelCount(ChannelCount::Mono);outputBuilder.setPerformanceMode(PerformanceMode::LowLatency); // 指定为低延迟outputBuilder.setSharingMode(SharingMode::Shared);outputBuilder.setFormat(AudioFormat::I16);outputBuilder.setSampleRate(sampleRate);outputBuilder.setFramesPerCallback(framesPerBuffer);result = outputBuilder.openStream(&playStream);if(result != Result::OK){LOGE("create output stream failed");return false;}this->inputApi = recordStream->getAudioApi();if(inputApi == AudioApi::OpenSLES){LOGD("oboe recorder use api OpenSLES");}else if(inputApi == AudioApi::AAudio){LOGD("oboe recorder use api AAudio");} else{LOGD("oboe recorder use api UNKNOWN");}this->outputApi = playStream->getAudioApi();if(inputApi == AudioApi::OpenSLES){LOGD("oboe player use api OpenSLES");}else if(inputApi == AudioApi::AAudio){LOGD("oboe player use api AAudio");} else{LOGD("oboe player use api UNKNOWN");}return true;}

注意:当启动回放时,由于是先启动player,因此player的线程会在回调函数里阻塞等待recorder启动并放入音频数据。但是在OpenSL ES那章讲过,OpenSL ES的player在启动时,需要手动Enqueue一个空buffer进去才能正常播放。Oboe也是这么做的。因此如果Oboe使用的是OpenSL ES进行播放,那么这个流程会导致死锁,我们可以看一下Oboe的源码。由于是指定了OpenSL ES进行播放,因此Oboe内部是使用AudioOutputStreamOpenSLES实现的。看一下它的requestStart方法:

Result AudioOutputStreamOpenSLES::requestStart() {LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__);mLock.lock();StreamState initialState = getState();switch (initialState) {case StreamState::Starting:case StreamState::Started:mLock.unlock();return Result::OK;case StreamState::Closed:mLock.unlock();return Result::ErrorClosed;default:break;}// We use a callback if the user requests one// OR if we have an internal callback to read the blocking IO buffer.setDataCallbackEnabled(true);setState(StreamState::Starting);Result result = setPlayState_l(SL_PLAYSTATE_PLAYING);if (result == Result::OK) {setState(StreamState::Started);mLock.unlock();if (getBufferDepth(mSimpleBufferQueueInterface) == 0) {// Enqueue the first buffer if needed to start the streaming.// This might call requestStop() so try to avoid a recursive lock.// 注意到这里,它进行了callback相关的操作,进去看一下。processBufferCallback(mSimpleBufferQueueInterface);}} else {setState(initialState);mLock.unlock();}return result;
}// 在processBufferCallback下有这行代码,它是调用callback获取了一个buffer数据。
// Ask the app callback to process the buffer.
DataCallbackResult result = fireDataCallback(mCallbackBuffer.get(), mFramesPerCallback);

可以看到,在stream的requestStart方法中,就已经调用了回调函数去获取数据了,因此如果在输入流没有启动之前,就让输出流的回调函数一直阻塞,那么输出流的requestStart方法就会被一直阻塞,导致无法启动输入流。

因此,对于Oboe使用OpenSL ES的情况,我是如下实现启动函数和回调函数的。

void OboeEcho::start() {if(!recordStream || !playStream){LOGE("player or recorder not prepared");return;}playFlag = true;/** 如果指定OpenSLES,则该标识为客户端是否刚启动播放。因为OpenSLES在启动时需要手动Enqueue一个空buffer。* 并且这个Enqueue的过程不是异步的,它会阻塞start方法。* 而由于我们希望尽可能降低播放延迟,因此都是首先启动play流。此时音频数据buffer是空的,play方法就会阻塞在* 回调函数里,等待录音流放入数据。但是play的start方法一直阻塞导致无法进行到recorder的start方法,就会发生死锁。* */justStart = true;playStream->start(10 * NANO_SEC_IN_MILL_SEC);recordStream->start(10 * NANO_SEC_IN_MILL_SEC);}DataCallbackResult
OboeEcho::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {if(!playFlag){return DataCallbackResult::Continue;}if(oboeStream == playStream){LOGD("player callback called");int32_t readSize = buffer->getRange((int16_t *)audioData, numFrames, !justStart);if(justStart){justStart = false;}
//        int32_t readSize = 0;
//        sleep(2);LOGD("player get data, readSize = %d", readSize);if(readSize != numFrames){return DataCallbackResult::Continue;}} else{LOGD("recorder callback called");int32_t writeSize = buffer->putAll((int16_t *)audioData, numFrames);LOGD("recorder write data, writeSize = %d", writeSize);if(writeSize != numFrames){return DataCallbackResult::Continue;}}return DataCallbackResult::Continue;
}

注意到buffer->getRange((int16_t *)audioData, numFrames, !justStart)这个函数调用,这里我使用的是环形BlockRingBuffer,该方法的最后一个bool的意思为是否阻塞该方法,即如果此时buffer里面的数据没有达到要求的那么多个,它就不会等待,而是立即返回已经拿到的数据个数。这样player的回调在第一次调用时就不会被阻塞。

7. 功能配置

别忘了先请求录音权限

native-lib.cpp中,我指定了一个参数可以启动哪个echo:

extern "C" JNIEXPORT jboolean JNICALL
Java_com_zu_fastpathaudioecho_MainActivity_nInit(JNIEnv *env, jobject instance, jint sampleRate, jint framesPerBuffer, jint api)
{if(echo != nullptr){return false;}if(api == 0){echo = new SLESEcho();return echo->init(sampleRate, framesPerBuffer);} else if(api == 1){echo = new AAudioEcho();return echo->init(sampleRate, framesPerBuffer);} else if(api >= 2){echo = new OboeEcho();AudioApi audioApi = api == 2 ? AudioApi::OpenSLES : AudioApi::AAudio;return ((OboeEcho *)echo)->init(sampleRate, audioApi, framesPerBuffer);}
}

至此就结束了,native音频这里确实还是比较多坑的,踩下来需要很多耐心。详细源码可以去我的github上下载。

总体来说,OpenSL ES使用起来颇为繁琐,文档查看起来也不够详尽,很多配置很难完全了解清楚。但它的优势在于支持广泛,并且功能强大。当然由于这是ES版本,不可能在移动端实现全部的OpenSL库功能。

AAudio用起来比较省心一点,但因为是在Android 8.0才推出的,所以在照顾老机型方面需要慎重考虑(魅族pro7,同年的一加3t已经到9.0了,但是pro7居然还是7.0。这样的厂商真是开发者的噩梦)。

而Oboe则是集大成者,通常情况下,如果你不指定所使用的API,那么框架会自动进行选择,非常省心。所以除非要加一些依赖于OpenSL ES的效果,否则使用Oboe是最好的。

Android native音频:录制播放的实现以及低延迟音频方案相关推荐

  1. 【Oboe——Android低延迟音频应用开发库使用介绍】

    Oboe--Android低延迟音频应用开发库使用介绍 一. 背景 Oboe是一个C++库,是Google于2018年开发用来为Android打造高性能的互动音频体验,可在99%的安卓设备上实现最低可 ...

  2. 低延迟音频中的音频解码优化策略

    文章目录 前言 音频播放 举个例子:PortAudio 回调函数 解码与播放 优化策略 1. 一次性读取音频到内存中 2. MMAP 3. 音频转码,再接 MMAP 4. 解码缓冲 总结 参考资料 前 ...

  3. AVFoundation 文本转语音和音频录制 播放

    现在你应该对AVFoundation有了比较深入的了解,并且对数字媒体的细节也有了一定认识,下面介绍一下 AVFoundation的文本转语音功能 AVSpeechSynthesizer 开发者可以使 ...

  4. 【小程序】PCM音频录制播放小工具

    VS2010工程源码下载链接: https://pan.baidu.com/s/1Vf6FOISDXDjORyLcQqCErw PCM是windows系统录音后得到的纯音频数据,需要添加头部说明信息才 ...

  5. php 音频顺序播放,html5 Audio多个mp3音频顺序播放

    我现在用的就是单个音频一直循环播放,就想着能不能更加多样化点,于是就有了这篇文章 实现多个html5音频顺序播放,div+js window.onload = function(){ var arr ...

  6. Opus:IETF低延迟音频编解码器:API和操作手册

    https://www.zybuluo.com/khan-lau/note/383775 Opus简介 Opus编解码器是专门设计用于互联网的交互式语音和音频传输.它是由IETF的编解码器工作组设计的 ...

  7. 海康大华等网络摄像机监控视频RTSP/RTMP推流网页播放/直播无需插件低延迟解决方案研究

    市面上常见监控视频推流方案简介 当前如果想要将监控视频在浏览器中播放,有几种常见的办法如下: 1.获取摄像头RTSP流,使用FFmpeg或者程序如JavaCV或者其他方式,将其推流成RTMP,通过服务 ...

  8. 3、JACK Audio Connect Kit低延迟音频服务——Qjackctl基本设置

    如windows的ASIO驱动一样,LINUX的alsa驱动也是独占声卡的,当有一个应用占用声卡时,其他应用程序将不能正常调用alsa驱动.不过大部分应用都可以选择以jack方式输出音频.他就像是一个 ...

  9. 华为手机吃鸡隐藏功能android,什么蓝牙耳机适合华为手机?超低延迟吃鸡蓝牙耳机安卓苹果通用...

    原标题:什么蓝牙耳机适合华为手机?超低延迟吃鸡蓝牙耳机安卓苹果通用 什么蓝牙耳机适合华为手机?超低延迟吃鸡蓝牙耳机安卓苹果通用 随着各种自媒体.手游.短视频越来越流行,大家对使用便捷的蓝牙耳机的需求也 ...

最新文章

  1. jenkins搭建cc++自动化构建
  2. 计算机应用基础试题事业单位,机关事业单位技术工人计算机应用基础知识复习题...
  3. 中国计算机学会CCF推荐国际学术会议和期刊目录-计算机网络
  4. 缓存和字符串相互转换
  5. rsync安装与配置使用 数据同步方案(centos6.5)
  6. RISC-V工具链环境(基于Debian/Linux操作系统)
  7. php获取邮箱内容吗,php正则验证email邮箱及抽取内容中email的例子
  8. 这两天有点热吆,star直线上涨!~Jeecg Boot
  9. 通过交互式命令从github拉取项目模板并创建新项目
  10. oracle简单建库基本流程
  11. ATL WTL 实现分析(四)
  12. 自动驾驶决策规划研究综述
  13. SAR—距离向脉冲压缩的一些理解
  14. c语言青蛙跳答案是多少啊,青蛙跳台阶问题(示例代码)
  15. 2017 linux wine 迅雷,Ubuntu+Wine+迅雷+QQ安装方法
  16. 变额年金(一、 递增年金)
  17. 计算机视觉注意力机制-Attention
  18. H5后台读写CAD文件
  19. 常见字读音(粤语)---(2)
  20. 全媒舍:活动策划的几个要点与常用做法

热门文章

  1. JQUERY--图片轮换superslide(
  2. 超级楼梯[HDU2041]
  3. Apache ActiveMQ消息中间件的基本使用
  4. linux shell 流程控制(条件if,循环【for,while】,选择【case】语句实例
  5. Perl 标量的操作符
  6. 单双号限行,今天是否绿色出行
  7. cmake安装配置及入门指南
  8. [c/c++] programming之路(12)、循环结构
  9. 【JavaScript】各种事件
  10. 汉语编程-现存的可能误区及可能方向思考