音頻采集:Android基于OpenSL ES的實(shí)現(xiàn)

前言

這篇文章簡單介紹下移動(dòng)端Android系統(tǒng)下利用OpenSL ES進(jìn)行音頻采集方法移剪。
按照慣例先上一份源碼 AudioRecordLib 苦银。
OpenSL ES采集的核心實(shí)現(xiàn)在于 openslescore.cpp 這個(gè)文件幔虏。

權(quán)限申請(qǐng)

想要使用OpenSL ES,需要在AndroidManifest.xml的配置文件里面增加權(quán)限

<uses-permission android:name="android.permission.RECORD_AUDIO"/>

OpenSL ES開發(fā)簡介

什么是OpenSL ES

OpenSL ES全稱為Open Sound Library for Embedded Systems贝椿,即嵌入式音頻加速標(biāo)準(zhǔn)瑟蜈。OpenSL ES是無授權(quán)費(fèi)渣窜、跨平臺(tái)乔宿、針對(duì)嵌入式系統(tǒng)精心優(yōu)化的硬件音頻加速 API。它為嵌入式移動(dòng)多媒體設(shè)備上的本地 應(yīng)用程序開發(fā)者提供了標(biāo)準(zhǔn)化坝橡、高性能计寇、低響應(yīng)時(shí)間的音頻功能實(shí)現(xiàn)方法,同時(shí)還實(shí)現(xiàn)了軟/硬件音頻性能的直接跨平臺(tái)部署狼讨,不僅降低了執(zhí)行難度朽基,而且促進(jìn)了高級(jí)音頻市場的發(fā)展稼虎。

OpenSL ES架構(gòu)原理

雖然OpenSL ES是基于C語言設(shè)計(jì)的API沉眶,但是其實(shí)基于對(duì)象和接口提供服務(wù)的,采用了面向?qū)ο蟮乃枷雭黹_發(fā)API捌肴。
這里簡單說一下OpenSL ES里面的對(duì)象和接口的概念:

  • 對(duì)象:類似于C++中類用來提供一組資源極其狀態(tài)的抽象孽查,也就是我們可以根據(jù)特定類型type(例如音頻錄制type)來獲取一個(gè)音頻錄制的對(duì)象洲胖,但是對(duì)于這個(gè)對(duì)象我們并不能直接操作(換句話講也就是我們不能直接在這個(gè)對(duì)象調(diào)用開始/結(jié)束錄制的邏輯)腐晾。
  • 接口:接口是對(duì)象提供一組特定功能方法的抽象巨柒,也就是可以從對(duì)象中獲取接口(例如從錄制對(duì)象中獲取錄制接口)珍坊,然后通過接口來改變對(duì)象的狀態(tài)(例如通過接口設(shè)置開始錄制)以便使用對(duì)象的功能(對(duì)于就是錄制功能)。

PS:對(duì)象可以有一個(gè)或者多個(gè)接口的實(shí)例,但是接口實(shí)例肯定只屬于一個(gè)對(duì)象,以上就是OpenSL ES的開發(fā)理念。

引用相關(guān)庫文件以及頭文件

怎么導(dǎo)入OpenSL ES庫

CMake方式:CMakeList.txt中加入

#找打Android lib庫里面的libOpenSLES.so的庫
find_library( OpenSLES-lib
                OpenSLES )
#鏈接到你的native工程的庫
target_link_libraries( your-native.so
                       ${OpenSLES-lib}
                     )

NDK Build方式:在Makefile文件Android.mk添加鏈接選項(xiàng)

LOCAL_LDLIBS = -lOpenSLES

引入頭文件

#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>

創(chuàng)建引擎對(duì)象

簡單介紹下入口slCreateEngine()這個(gè)全局方法:

SL_API SLresult SLAPIENTRY slCreateEngine(
    SLObjectItf             *pEngine,           //對(duì)象地址洪灯,用于傳出對(duì)象
    SLuint32                numOptions,         //配置參數(shù)數(shù)量
    const SLEngineOption    *pEngineOptions,    //配置參數(shù),為枚舉數(shù)組
    SLuint32                numInterfaces,      //支持的接口數(shù)量
    const SLInterfaceID     *pInterfaceIds,     //具體的要支持的接口憎夷,是枚舉的數(shù)組
    const SLboolean         *pInterfaceRequired //具體的要支持的接口是開放的還是關(guān)閉的,也是一個(gè)數(shù)組级及,這三個(gè)參數(shù)長度是一致的
);

一個(gè)較為完整的創(chuàng)建過程代碼示例:

SLObjectItf engine_object;   //引擎對(duì)象
SLEngineItf engine_engine;   //引擎接口

//調(diào)用全局方法創(chuàng)建一個(gè)引擎對(duì)象(OpenSL ES唯一入口)
slCreateEngine(&engine_object, 0, NULL, 0, NULL, NULL);
//實(shí)例化這個(gè)對(duì)象
(*engine_object)->Realize(engine_object, SL_BOOLEAN_FALSE);
//從這個(gè)對(duì)象里面獲取引擎接口
(*engine_object)->GetInterface(engine_object, SL_IID_ENGINE, &engine_engine);

當(dāng)然調(diào)用每一個(gè)API后要檢測其返回值是否等于 SL_RESULT_SUCCESS县踢,限于篇幅就在上面代碼沒有處理,后續(xù)的示例代碼也是同理。

設(shè)置IO設(shè)備(麥克風(fēng)) 輸入輸出

我們需要設(shè)置采集設(shè)備的一些輸入輸出配置:

//設(shè)置IO設(shè)備(麥克風(fēng))
SLDataLocator_IODevice io_device = {
        SL_DATALOCATOR_IODEVICE,         //類型 這里只能是SL_DATALOCATOR_IODEVICE
        SL_IODEVICE_AUDIOINPUT,          //device類型  選擇了音頻輸入類型
        SL_DEFAULTDEVICEID_AUDIOINPUT,   //deviceID 對(duì)應(yīng)的是SL_DEFAULTDEVICEID_AUDIOINPUT
        NULL                             //device實(shí)例
};
SLDataSource data_src = {
        &io_device,                      //SLDataLocator_IODevice配置輸入
        NULL                             //輸入格式拯爽,采集的并不需要
};

//設(shè)置輸出buffer隊(duì)列
SLDataLocator_AndroidSimpleBufferQueue buffer_queue = {
        SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,    //類型 這里只能是SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE
        2                                           //buffer的數(shù)量
};
//設(shè)置輸出數(shù)據(jù)的格式
SLDataFormat_PCM format_pcm = {
        SL_DATAFORMAT_PCM,                             //輸出PCM格式的數(shù)據(jù)
        num_channels,                                  //輸出的聲道數(shù)量
        SL_SAMPLINGRATE_44_1,                          //輸出的采樣頻率,這里是44100Hz
        SL_PCMSAMPLEFORMAT_FIXED_16,                   //輸出的采樣格式,這里是16bit
        SL_PCMSAMPLEFORMAT_FIXED_16,                   //一般來說,跟隨上一個(gè)參數(shù)
        SL_SPEAKER_FRONT_LEFT|SL_SPEAKER_FRONT_RIGHT,  //雙聲道配置焊傅,如果單聲道可以用 SL_SPEAKER_FRONT_CENTER
        SL_BYTEORDER_LITTLEENDIAN                      //PCM數(shù)據(jù)的大小端排列
};
SLDataSink audioSink = {
        &buffer_queue,                   //SLDataFormat_PCM配置輸出
        &format_pcm                      //輸出數(shù)據(jù)格式
};

創(chuàng)建錄制器

主要是創(chuàng)建錄制對(duì)象和獲取錄制相關(guān)的接口:

SLObjectItf recorder_object;                         //錄制對(duì)象,這個(gè)對(duì)象我們從里面獲取了2個(gè)接口
SLRecordItf recorder_recoder;                        //錄制接口
SLAndroidSimpleBufferQueueItf recorder_buffer_queue; //Buffer接口

//創(chuàng)建錄制的對(duì)象骆姐,并且指定開放SL_IID_ANDROIDSIMPLEBUFFERQUEUE這個(gè)接口
const SLInterfaceID id[1] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
const SLboolean req[1] = {SL_BOOLEAN_TRUE};
(*engine_engine)->CreateAudioRecorder(engine_engine,        //引擎接口
                                        &recorder_object,   //錄制對(duì)象地址,用于傳出對(duì)象
                                        &data_src,          //輸入配置
                                        &audioSink,         //輸出配置
                                        1,                  //支持的接口數(shù)量
                                        id,                 //具體的要支持的接口
                                        req                 //具體的要支持的接口是開放的還是關(guān)閉的
                                        );
//實(shí)例化這個(gè)錄制對(duì)象
(*recorder_object)->Realize(recorder_object, SL_BOOLEAN_FALSE);
//獲取錄制接口
(*recorder_object)->GetInterface(recorder_object, SL_IID_RECORD, &recorder_recoder);
//獲取Buffer接口
(*recorder_object)->GetInterface(recorder_object, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &recorder_buffer_queue);

設(shè)置數(shù)據(jù)回調(diào)并且開始錄制

設(shè)置開始錄制狀態(tài),并通過回調(diào)函數(shù)獲取錄制的音頻PCM數(shù)據(jù):

int8_t *pcm_data; //數(shù)據(jù)緩存區(qū)
//申請(qǐng)一塊內(nèi)存窟社,注意RECORDER_FRAMES是自定義的一個(gè)宏,指的是采集的frame數(shù)量,具體還要根據(jù)你的采集格式(例如16bit)計(jì)算
pcm_data = static_cast<int8_t *>(malloc(sizeof(int8_t) * RECORDER_FRAMES));

//設(shè)置數(shù)據(jù)回調(diào)接口bqRecorderCallback寸潦,最后一個(gè)參數(shù)是可以傳輸自定義的上下文引用
(*recorder_buffer_queue)->RegisterCallback(recorder_buffer_queue, bqRecorderCallback, this);
//設(shè)置錄制器為錄制狀態(tài) SL_RECORDSTATE_RECORDING
(*recorder_recoder)->SetRecordState(recorder_recoder, SL_RECORDSTATE_RECORDING);
/在設(shè)置完錄制狀態(tài)后一定需要先Enqueue一次色鸳,這樣的話才會(huì)開始采集回調(diào)
(*recorder_buffer_queue)->Enqueue(recorder_buffer_queue, pcm_data, RECORDER_FRAMES);

//數(shù)據(jù)回調(diào)函數(shù)
static void bqRecorderCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
    //從pcm_data獲取RECORDER_FRAMES長度的PCM數(shù)據(jù)
    //注意這個(gè)是另外一條采集線程回調(diào),可能需要加一個(gè)變量recodering控制退出采集
    if (recodering)
    {
        pcm_write(pcm_data, RECORDER_FRAMES);
        //取完數(shù)據(jù)见转,需要調(diào)用Enqueue觸發(fā)下一次數(shù)據(jù)回調(diào)
        (*recorder_buffer_queue)->Enqueue(recorder_buffer_queue, pcm_data, RECORDER_FRAMES);
    }
}

停止錄制和釋放OpenSL ES資源

如果我們不需要采集了赊抖,需要調(diào)用接口停止采集并在適當(dāng)?shù)臅r(shí)機(jī)釋放OpenSL ES相關(guān)資源浴鸿。

//設(shè)置錄制器為停止?fàn)顟B(tài) SL_RECORDSTATE_STOPPED
(*recorder_recoder)->SetRecordState(recorder_recoder, SL_RECORDSTATE_STOPPED);

//只需要銷毀OpenSL ES對(duì)象,接口不需要做Destroy處理。
(*recorder_object)->Destroy(recorder_object);
(*engine_object)->Destroy(engine_object);

//釋放緩存區(qū)內(nèi)存
free(pcm_data);

這樣就完整的結(jié)束了OpenSL ES的采集業(yè)務(wù)。

播放PCM文件

Audacity這個(gè)工具可以導(dǎo)入pcm原始文件薇缅,并且提供了波形圖查看和播放功能危彩。
操作流程是:
文件 => 導(dǎo)入 => 原始數(shù)據(jù) => 設(shè)置PCM數(shù)據(jù)格式 => 導(dǎo)入
具體效果圖如下:

p1.png

結(jié)語

上一篇博客了介紹了Android利用AudioRecord進(jìn)行錄音導(dǎo)出PCM數(shù)據(jù)。
本文同步發(fā)布于簡書泳桦、CSDN汤徽。

End!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市灸撰,隨后出現(xiàn)的幾起案子谒府,更是在濱河造成了極大的恐慌,老刑警劉巖浮毯,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件完疫,死亡現(xiàn)場離奇詭異,居然都是意外死亡债蓝,警方通過查閱死者的電腦和手機(jī)壳鹤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饰迹,“玉大人芳誓,你說我怎么就攤上這事余舶。” “怎么了锹淌?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵匿值,是天一觀的道長。 經(jīng)常有香客問我葛圃,道長千扔,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任库正,我火速辦了婚禮曲楚,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘褥符。我一直安慰自己龙誊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布喷楣。 她就那樣靜靜地躺著趟大,像睡著了一般。 火紅的嫁衣襯著肌膚如雪铣焊。 梳的紋絲不亂的頭發(fā)上逊朽,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音曲伊,去河邊找鬼叽讳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛坟募,可吹牛的內(nèi)容都是我干的岛蚤。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼懈糯,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼涤妒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起赚哗,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤她紫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后屿储,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贿讹,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年扩所,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了围详。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凿掂,死狀恐怖郁稍,靈堂內(nèi)的尸體忽然破棺而出净刮,到底是詐尸還是另有隱情榜跌,我是刑警寧澤衔掸,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布瓦哎,位于F島的核電站蛀序,受9級(jí)特大地震影響护侮,放射性物質(zhì)發(fā)生泄漏群叶。R本人自食惡果不足惜吃挑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望街立。 院中可真熱鬧舶衬,春花似錦、人聲如沸赎离。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽梁剔。三九已至虽画,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間荣病,已是汗流浹背码撰。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留个盆,地道東北人脖岛。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像砾省,于是被迫代替她去往敵國和親鸡岗。 傳聞我的和親對(duì)象是個(gè)殘疾皇子混槐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容