人間觀察
勿再別人的心中修行自己,
勿再自己的心中強求別人。
前言
最近寫文章有點偷懶了,離上次寫文章大概一個月了。
一般Android音頻的采集在java層使用AudioRecord類進行采集。
但是為什么要學(xué)OpenSL呢?除了C/C++的性能優(yōu)勢(不過其實java的效率也不低)之外,最主要是你如果使用java層的接口,還需要通過一層JNI,比較復(fù)雜,性能消耗也大。如果用OpenSL的話就能直接在C/C++里面把事情都處理了。所以有時候為了開發(fā)更加高效的 Android 音頻應(yīng)用需要在底層直接進行錄音采集播放等,免去java和jni層的互相通信。
那這篇文章主要介紹Android在JNI層如何使用OpenSL ES進行音頻的采集。
OpenSL ES介紹
此節(jié)介紹有些摘自網(wǎng)絡(luò),只為學(xué)習(xí)使用,只為更好更全的介紹下OpenSL ES。如有侵權(quán)將刪除!
什么是OpenSL ES
這個在前一篇文章的利用OpenSL ES播放pcm數(shù)據(jù)的時候也有所介紹。這里再介紹下。
OpenSL ES全稱為Open Sound Library for Embedded Systems,即嵌入式音頻加速標準。OpenSL ES是無授權(quán)費、跨平臺、針對嵌入式系統(tǒng)精心優(yōu)化的硬件音頻加速 API。它為嵌入式移動多媒體設(shè)備上的本地 應(yīng)用程序開發(fā)者提供了標準化、高性能、低響應(yīng)時間的音頻功能實現(xiàn)方法,同時還實現(xiàn)了軟/硬件音頻性能的直接跨平臺部署,不僅降低了執(zhí)行難度,而且促進了高級音頻市場的發(fā)展。簡單來說OpenSL ES是一個嵌入式跨平臺免費的音頻處理庫。 所以它不是Android特有的。
OpenSL ES 與 Android的關(guān)系

官網(wǎng)介紹地址:https://source.android.com/devices/audio/latency_app.html
可以看到Android 實現(xiàn)的 OpenSL ES 只是 OpenSL 1.0.1 的子集,并且進行了擴展,因此,對于 OpenSL ES API 的使用,我們還需要特別留意哪些是 Android 支持的,哪些是不支持的。
OpenSL ES的特性以及優(yōu)劣勢
特性:
(1)C 語言接口,兼容 C++,需要在 NDK 下開發(fā),能更好地集成在 native 應(yīng)用中
(2)運行于 native 層,需要自己管理資源的申請與釋放,沒有 Dalvik 虛擬機的垃圾回收機制
(3)支持 PCM 數(shù)據(jù)的采集,支持的配置:16bit 位寬,16000 Hz采樣率,單通道。(其他的配置不能保證兼容所有平臺)
(4)支持 PCM 數(shù)據(jù)的播放,支持的配置:8bit/16bit 位寬,單通道/雙通道,小端模式,采樣率(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz)
(5)支持播放的音頻數(shù)據(jù)來源:res 文件夾下的音頻、assets 文件夾下的音頻、sdcard 目錄下的音頻、在線網(wǎng)絡(luò)音頻、代碼中定義的音頻二進制數(shù)據(jù)等等
優(yōu)勢:
(1)避免音頻數(shù)據(jù)頻繁在 native 層和 Java 層拷貝,提高效率
(2)相比于 Java API,可以更靈活地控制參數(shù)
(3)由于是 C 代碼,因此可以做深度優(yōu)化,比如采用 NEON 優(yōu)化
(4)代碼細節(jié)更難被反編譯
劣勢:
(1)不支持版本低于 Android 2.3 (API 9) 的設(shè)備
(2)沒有全部實現(xiàn) OpenSL ES 定義的特性和功能
(3)不支持 MIDI
(4)不支持直接播放 DRM 或者 加密的內(nèi)容
(5)不支持音頻數(shù)據(jù)的編解碼,如需編解碼,需要使用 MediaCodec API 或者第三方庫
(6)在音頻延時方面,相比于上層 API,并沒有特別明顯地改進
更多介紹
可以看下android 中文官網(wǎng)。里面也有一些demo
https://developer.android.google.cn/ndk/guides/audio/opensl
ok,了解了OpenSL ES 的 一些背景后,我們接下來開始介紹OpenSL ES 的API和實現(xiàn)錄音采集功能。
添加權(quán)限
要想錄音無論是使用java層的AudioRecord還是底層的OpenSL ES都需要在AndroidManifest.xml的配置文件里面增加錄音權(quán)限。
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
引用相關(guān)庫文件以及頭文件
在Android中使用OpenSLES首先需要把Android 系統(tǒng)提供的so鏈接到外面自己的so。在CMakeLists.txt腳本中添加鏈接庫OpenSLES。庫的名字可以在你的ndk目錄下找到,類似目錄 :
/Users/guxiuzhong/Library/Android/sdk/ndk/21.1.6352462/platforms/android-19/arch-x86/usr/lib/libOpenSLES.so
然后需要去掉lib前綴即可。當工程編譯的時候它會自動找這個目錄的。
CMake方式 在CMakeLists.txt中配置
target_link_libraries(
OpenSLES
// ...省略其它需要鏈接的so
)
NDK Build方式 在Makefile文件Android.mk添加鏈接選項
LOCAL_LDLIBS += -lOepnSLES
頭文件添加
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
OpenSLES重要概念
Objects和Interfaces
在OpenSL ES的開發(fā)中有兩個必須理解的概念,一個是Object,另一個是Interface。因為很多的API都是在操作Object和Interface。Android為了更方便的使用OpenSL ES把OpenSL ES 的API設(shè)計成了類似面向?qū)ο蟮膉ava的使用方式了。Object 可以想象成 Java 的 Object 類,Interface 可以想象成 Java 的 Interface,但它們并不完全相同。他們的關(guān)系:
(1) 每個 Object 可能會存在一個或者多個 Interface,官方為每一種 Object 都定義了一系列的 Interface
(2)每個 Object 對象都提供了一些最基礎(chǔ)的操作,比如:Realize,Resume,GetState,Destroy 等等,如果希望使用該對象支持的功能函數(shù),則必須通過其 GetInterface 函數(shù)拿到 Interface 接口,然后通過 Interface 來訪問功能函數(shù)
(3)并不是每個系統(tǒng)上都實現(xiàn)了 OpenSL ES 為 Object 定義的所有 Interface,所以在獲取 Interface 的時候需要做一些選擇和判斷
所有的Object在OpenSL里面我們拿到的都是一個SLObjectItf:
typedef const struct SLObjectItf_ * const * SLObjectItf;
struct SLObjectItf_ {
SLresult (*Realize) (
SLObjectItf self,
SLboolean async
);
SLresult (*Resume) (
SLObjectItf self,
SLboolean async
);
SLresult (*GetState) (
SLObjectItf self,
SLuint32 * pState
);
SLresult (*GetInterface) (
SLObjectItf self,
const SLInterfaceID iid,
void * pInterface
);
SLresult (*RegisterCallback) (
SLObjectItf self,
slObjectCallback callback,
void * pContext
);
void (*AbortAsyncOperation) (
SLObjectItf self
);
void (*Destroy) (
SLObjectItf self
);
SLresult (*SetPriority) (
SLObjectItf self,
SLint32 priority,
SLboolean preemptable
);
SLresult (*GetPriority) (
SLObjectItf self,
SLint32 *pPriority,
SLboolean *pPreemptable
);
SLresult (*SetLossOfControlInterfaces) (
SLObjectItf self,
SLint16 numInterfaces,
SLInterfaceID * pInterfaceIDs,
SLboolean enabled
);
};
任何創(chuàng)建出來的Object都必須調(diào)用 Realize 方法做初始化,在不需要的時候可以使用 Destroy 方法來釋放資源。
GetInterface
GetInterface可以說是OpenSL里使用頻率最高的方法,通過它我們可以獲取Object里面的Interface。
由于一個Object里面可能包含了多個Interface,所以GetInterface方法有個SLInterfaceID參數(shù)來指定到的需要獲取Object里面的那個Interface。
比如我們通過EngineObject去獲取SL_IID_ENGINE這個id的Interface,而這個id對應(yīng)的Interface就是SLEngineItf:
SLEngineItf engineEngine = NULL;
SLObjectItf engineObject = NULL;
// 創(chuàng)建引擎對象,調(diào)用全局方法創(chuàng)建一個引擎對象(OpenSL ES唯一入口)
SLresult result;
result = slCreateEngine(&engineObject, 1, pEngineOptions, 0, nullptr, nullptr);
assert(SL_RESULT_SUCCESS == result);
/* Realizing the SL Engine in synchronous mode. */
//實例化這個對象
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
// get the engine interface, which is needed in order to create other objects
//從這個對象里面獲取引擎接口
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
assert(SL_RESULT_SUCCESS == result);
當調(diào)用每一個API后要檢測其返回值是否等于 「SL_RESULT_SUCCESS」
Interface
Interface則是方法的集合,例如SLRecordItf里面包含了和錄音相關(guān)的方法,SLPlayItf包含了和播放相關(guān)的方法。我們功能都是通過調(diào)用Interfaces的方法去實現(xiàn)的。
如 SLEngineItf 這個interface,SLEngineItf是OpenSL里面最重要的一個Interface,我們可以通過它去創(chuàng)建各種Object,例如播放器、錄音器、混音器的Object,然后在用這些Object去獲取各種Interface去實現(xiàn)各種功能。
比如:
SLresult (*CreateAudioPlayer) (
SLEngineItf self,
SLObjectItf * pPlayer,
SLDataSource *pAudioSrc,
SLDataSink *pAudioSnk,
SLuint32 numInterfaces,
const SLInterfaceID * pInterfaceIds,
const SLboolean * pInterfaceRequired
);
SLresult (*CreateAudioRecorder) (
SLEngineItf self,
SLObjectItf * pRecorder,
SLDataSource *pAudioSrc,
SLDataSink *pAudioSnk,
SLuint32 numInterfaces,
const SLInterfaceID * pInterfaceIds,
const SLboolean * pInterfaceRequired
);
// 還有很多其它的,可以看下OpenSLES.h的頭文件
Object的生命周期
OpenSL ES 的 Object 一般有三種狀態(tài),分別是:UNREALIZED (不可用),REALIZED(可用),SUSPENDED(掛起)。
Object 處于 UNREALIZED (不可用)狀態(tài)時,系統(tǒng)不會為其分配資源;調(diào)用 Realize 方法后便進入 REALIZED(可用)狀態(tài),此時對象的各個功能和資源可以正常訪問;當系統(tǒng)音頻相關(guān)的硬件設(shè)備被其他進程占用時,OpenSL ES Object 便會進入 SUSPENDED (掛起)狀態(tài),隨后調(diào)用 Resume 方法可使對象重回 REALIZED(可用)狀態(tài);當 Object 使用結(jié)束后,調(diào)用 Destroy 方法釋放資源,是對象重回 UNREALIZED (不可用)狀態(tài)。
錄音
demo就簡單的把采集到的pcm文件保存到sd卡目錄的文件下了,主要是如何用OpenSL ES實現(xiàn)音頻pcm數(shù)據(jù)的采集。
明白了上面的OpenSL ES的設(shè)計思想和Object和Interface的API使用流程代碼就容易看得懂了。
創(chuàng)建引擎對象
需要調(diào)用slCreateEngine()這個全局方法來創(chuàng)建
SL_API SLresult SLAPIENTRY slCreateEngine(
SLObjectItf *pEngine, // 引擎對象地址,用于傳出對象,是一個輸出參數(shù)
SLuint32 numOptions, //配置參數(shù)數(shù)量,傳1
const SLEngineOption *pEngineOptions,// 配置參數(shù),為枚舉數(shù)組
SLuint32 numInterfaces,//支持的接口數(shù)量
const SLInterfaceID *pInterfaceIds,//具體的要支持的接口,是枚舉的數(shù)組
const SLboolean * pInterfaceRequired//具體的要支持的接口是開放的還是關(guān)閉的,也是一個數(shù)組,后三個參數(shù)長度是一致的
);
創(chuàng)建之后按照上面介紹的標準流程三步走,調(diào)用Realize來實例化這個對象,之后通過GetInterface從這個對象里面獲取引擎接口。所以創(chuàng)建引擎對象的代碼片段為:
// 成員變量
SLEngineItf engineEngine = NULL;
SLObjectItf engineObject = NULL;
void AudioRecorder::createEngine() {
SLEngineOption pEngineOptions[] = {(SLuint32) SL_ENGINEOPTION_THREADSAFE,
(SLuint32) SL_BOOLEAN_TRUE};
// 創(chuàng)建引擎對象,//調(diào)用全局方法創(chuàng)建一個引擎對象(OpenSL ES唯一入口)
SLresult result;
result = slCreateEngine(
&engineObject, //對象地址,用于傳出對象
1, //配置參數(shù)數(shù)量
pEngineOptions, //配置參數(shù),為枚舉數(shù)組
0, //支持的接口數(shù)量
nullptr, //具體的要支持的接口,是枚舉的數(shù)組
nullptr//具體的要支持的接口是開放的還是關(guān)閉的,也是一個數(shù)組,這三個參數(shù)長度是一致的
);
assert(SL_RESULT_SUCCESS == result);
/* Realizing the SL Engine in synchronous mode. */
//實例化這個對象
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
// get the engine interface, which is needed in order to create other objects
//從這個對象里面獲取引擎接口
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
assert(SL_RESULT_SUCCESS == result);
}
設(shè)置采集設(shè)備(麥克風(fēng))輸入輸出配置
SLDataLocator_IODevice ioDevice = {
SL_DATALOCATOR_IODEVICE, //類型 這里只能是SL_DATALOCATOR_IODEVICE
SL_IODEVICE_AUDIOINPUT,//device類型 選擇了音頻輸入類型
SL_DEFAULTDEVICEID_AUDIOINPUT, //deviceID 對應(yīng)的是SL_DEFAULTDEVICEID_AUDIOINPUT
NULL//device實例
};
// 輸入,SLDataSource 表示音頻數(shù)據(jù)來源的信息
SLDataSource recSource = {
&ioDevice,//SLDataLocator_IODevice配置輸入
NULL//輸入格式,采集的并不需要
};
// 數(shù)據(jù)源簡單緩沖隊列定位器,輸出buffer隊列
SLDataLocator_AndroidSimpleBufferQueue recBufferQueue = {
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, //類型 這里只能是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
NUM_BUFFER_QUEUE //buffer的數(shù)量
};
// PCM 數(shù)據(jù)源格式 //設(shè)置輸出數(shù)據(jù)的格式
SLDataFormat_PCM pcm = {
SL_DATAFORMAT_PCM, //輸出PCM格式的數(shù)據(jù)
2, // //輸出的聲道數(shù)量2 個聲道(立體聲)
SL_SAMPLINGRATE_44_1, //輸出的采樣頻率,這里是44100Hz
SL_PCMSAMPLEFORMAT_FIXED_16, //輸出的采樣格式,這里是16bit
SL_PCMSAMPLEFORMAT_FIXED_16,//一般來說,跟隨上一個參數(shù)
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//雙聲道配置,如果單聲道可以用 SL_SPEAKER_FRONT_CENTER
SL_BYTEORDER_LITTLEENDIAN //PCM數(shù)據(jù)的大小端排列
};
// 輸出,SLDataSink 表示音頻數(shù)據(jù)輸出信息
SLDataSink dataSink = {
&recBufferQueue, //SLDataFormat_PCM配置輸出
&pcm //輸出數(shù)據(jù)格式
};
OpenSL ES 中的 SLDataSource 和 SLDataSink 結(jié)構(gòu)體,主要用于構(gòu)建 audio player 和 recorder 對象,其中 SLDataSource 表示音頻數(shù)據(jù)來源的信息,SLDataSink 表示音頻數(shù)據(jù)輸出信息。
創(chuàng)建錄音器-創(chuàng)建錄音對象和獲取錄音相關(guān)的接口
用SLEngineItf來創(chuàng)建錄音器,調(diào)用CreateAudioRecorder方法
//創(chuàng)建錄制的對象,并且指定開放SL_IID_ANDROIDSIMPLEBUFFERQUEUE這個接口
SLInterfaceID iids[NUM_RECORDER_EXPLICIT_INTERFACES] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
SL_IID_ANDROIDCONFIGURATION};
SLboolean required[NUM_RECORDER_EXPLICIT_INTERFACES] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
/* Create the audio recorder */
// 創(chuàng)建 audio recorder 對象
result = (*engineEngine)->CreateAudioRecorder(engineEngine, //引擎接口
&recorderObject, //錄制對象地址,用于傳出對象
&recSource,//輸入配置
&dataSink,//輸出配置
NUM_RECORDER_EXPLICIT_INTERFACES,//支持的接口數(shù)量
iids, //具體的要支持的接口
required //具體的要支持的接口是開放的還是關(guān)閉的
);
assert(SL_RESULT_SUCCESS == result);
assert(SL_RESULT_SUCCESS == result);
/* Realize the recorder in synchronous mode. */ //實例化這個錄制對象
result = (*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);
assert(SL_RESULT_SUCCESS == result);
/* Get the buffer queue interface which was explicitly requested *///獲取Buffer接口
result = (*recorderObject)->GetInterface(recorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
(void *) &recorderBuffQueueItf);
assert(SL_RESULT_SUCCESS == result);
/* get the record interface */ //獲取錄制接口
(*recorderObject)->GetInterface(recorderObject, SL_IID_RECORD, &recorderRecord);
assert(SL_RESULT_SUCCESS == result);
還是先創(chuàng)建出來的Object后,然后調(diào)用 Realize 方法做初始化,再調(diào)用GetInterface來獲取Object里面的那個Interface。SL_IID_RECORD就是錄音器的接口方法的id,輸出參數(shù)recorderRecord就是錄音接口,后續(xù)的啟動錄音停止錄音都是對它操作。
設(shè)置數(shù)據(jù)回調(diào)方法并且開始錄制
通過回調(diào)函數(shù)RegisterCallback后并設(shè)置開始錄制狀態(tài)來獲取錄制的音頻 PCM 數(shù)據(jù)。
buffer = new uint8_t[BUFFER_SIZE]; //數(shù)據(jù)緩存區(qū),
bufferSize = BUFFER_SIZE;
//設(shè)置數(shù)據(jù)回調(diào)接口AudioRecorderCallback,最后一個參數(shù)是可以傳輸自定義的上下文引用
(*recorderBuffQueueItf)->RegisterCallback(recorderBuffQueueItf, AudioRecorderCallback, this);
assert(SL_RESULT_SUCCESS == result);
/* Start recording */
// 開始錄制音頻,//設(shè)置錄制器為錄制狀態(tài) SL_RECORDSTATE_RECORDING
result = (*recorderRecord)->SetRecordState(recorderRecord, SL_RECORDSTATE_RECORDING);
assert(SL_RESULT_SUCCESS == result);
// 在設(shè)置完錄制狀態(tài)后一定需要先Enqueue一次,這樣的話才會開始采集回調(diào)
/* Enqueue buffers to map the region of memory allocated to store the recorded data */
(*recorderBuffQueueItf)->Enqueue(recorderBuffQueueItf, buffer, BUFFER_SIZE);
assert(SL_RESULT_SUCCESS == result);
LOGD("Starting recording tid=%ld", syscall(SYS_gettid));//線程id
其中RegisterCallback的第1個參數(shù)是獲取Buffer接口,第2個參數(shù)是一個函數(shù)的地址,錄音時
OpenSL ES 會自動進行回調(diào),需要注意的是回調(diào)方法AudioRecorderCallback不是在UI線程回調(diào),是一個子線程。第3個參數(shù)是給回調(diào)方法傳入的參數(shù),可以傳任何數(shù)據(jù),這里就傳入了this,方便在回調(diào)方法里獲取一些成員變量等?;卣{(diào)函數(shù)如下:
void AudioRecorderCallback(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {
//注意這個是另外一條采集線程回調(diào)
AudioRecorder *recorderContext = (AudioRecorder *) context;
assert(recorderContext != NULL);
if (recorderContext->buffer != NULL) {
fwrite(recorderContext->buffer, recorderContext->bufferSize, 1, recorderContext->pfile);
LOGD("save a frame audio data,pid=%ld", syscall(SYS_gettid));
SLresult result;
SLuint32 state;
result = (*(recorderContext->recorderRecord))->GetRecordState(
recorderContext->recorderRecord, &state);
assert(SL_RESULT_SUCCESS == result);
(void) result;
LOGD("state=%d", state);
if (state == SL_RECORDSTATE_RECORDING) {
//取完數(shù)據(jù),需要調(diào)用Enqueue觸發(fā)下一次數(shù)據(jù)回調(diào)
result = (*bufferQueueItf)->Enqueue(bufferQueueItf, recorderContext->buffer,
recorderContext->bufferSize);
assert(SL_RESULT_SUCCESS == result);
(void) result;
}
}
}
需要注意的點如注釋所寫: 在設(shè)置完錄制狀態(tài)后一定需要先Enqueue一次,這樣的話才會開始采集回調(diào),然后每處理完一次后需要再次Enqueue來達到循環(huán)錄音。
停止錄音
對于停止錄音,就是對錄音接口SLRecordItf修改錄制狀態(tài)為SL_RECORDSTATE_STOPPED即可
void AudioRecorder::stopRecord() {
// 停止錄制
if (recorderRecord != nullptr) {
//設(shè)置錄制器為停止狀態(tài) SL_RECORDSTATE_STOPPED
SLresult result = result = (*recorderRecord)->SetRecordState(recorderRecord,
SL_RECORDSTATE_STOPPED);
assert(SL_RESULT_SUCCESS == result);
fclose(pfile);
pfile = nullptr;
delete buffer;
LOGD("stopRecord done");
}
}
釋放錄音和OpenSL ES的資源
// 釋放資源,釋放OpenSL ES資源
void AudioRecorder::release() {
//只需要銷毀OpenSL ES對象,接口不需要做Destroy處理。
if (recorderObject != nullptr) {
(*recorderObject)->Destroy(recorderObject);
recorderObject = NULL;
recorderRecord = NULL;
recorderBuffQueueItf = NULL;
configItf = NULL;
}
// destroy engine object, and invalidate all associated interfaces
if (engineObject != NULL) {
// 釋放引擎對象的資源
(*engineObject)->Destroy(engineObject);
engineObject = NULL;
engineEngine = NULL;
}
LOGD("release done");
調(diào)用測試
有個Audacity軟件可以進行播放未壓縮的音頻pcm文件。當然你也可以用前一篇文章我們介紹的AudioTrack和OpenSL ES進行播放來驗證聲音是否ok。
JNI層如下代碼,比較簡單就貼一下了:
AudioRecorder *audioRecorder;
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_startRecord(JNIEnv *env, jobject thiz) {
if (audioRecorder == nullptr) {
audioRecorder = new AudioRecorder();
audioRecorder->startRecord();
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_stopRecord(JNIEnv *env, jobject thiz) {
if (audioRecorder != nullptr) {
audioRecorder->stopRecord();
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_bj_gxz_pcmplay_AudioRecorder_release(JNIEnv *env, jobject thiz) {
if (audioRecorder != nullptr) {
audioRecorder->release();
delete audioRecorder;
audioRecorder = nullptr;
}
}
對應(yīng)java層:
/**
* Created by guxiuzhong on 2021/06/05 2:03 下午
*/
public class AudioRecorder {
static {
System.loadLibrary("native-lib");
}
public native void startRecord();
public native void stopRecord();
public native void release();
}
源碼
https://github.com/ta893115871/PCMPlay
總結(jié)
學(xué)習(xí)了OpenSL ES的一些基礎(chǔ)知識,以及它的優(yōu)缺點,使用場景。
學(xué)習(xí)了OpenSL ES采集音頻的流程。