OpenSL ES 學(xué)習(xí)筆記

一般來講在安卓中常使用AudioRecord藐石、MediaRecorder對音頻進(jìn)行采集,使用SoundPool、MediaPlayer定拟、AudioTrack進(jìn)行音頻播放于微。

但是這些接口都是java層的,而NDK其實(shí)也提供了一個叫做OpenSL的C語言引擎用于聲音的處理。

OpenSL入門難度比較大,而且網(wǎng)上也沒有什么特別好的教程,我這里把自己了解到的一些知識記錄下來,希望以后忘記的時候可以快速回憶起來,也希望對大家有用青自。

這篇筆記的很多內(nèi)容都參考了OpenSL的官方文檔OpenSL_ES_Specification_1.0.1.pdf,它是全英文的,可以在NDK的安裝目錄下找到,大家可以大概瀏覽一下,具體路徑為:

$NDK_ROOT/docs/Additional_library_docs/opensles

為什么要學(xué)OpenSL呢?除了C/C++的性能優(yōu)勢(不過其實(shí)java的效率也不低)之外,最主要是因?yàn)樽罱肟覨Fmpeg,如果使用java層的接口,還需要通過一層JNI,比較復(fù)雜,性能消耗也大株依。如果用OpenSL的話就能直接在C/C++里面把事情都處理了。

基本概念

Object和Interface

在OpenSL里面,Object和Interface是兩個很重要的概念,基本上所有的操作都是通過它們兩個去執(zhí)行的延窜。

Object和Interface是包含關(guān)系,一個Object里面包含了多個Interface:

1.png

Object

Object是一個資源的抽象集合,可以通過它獲取各種資源勺三。

例如我們可以通過Object的GetInterface方法獲取Interface。

所有的Object在OpenSL里面我們拿到的都是一個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);
};

typedef const struct SLObjectItf_ * const * SLObjectItf;

在創(chuàng)建出來之后必須先調(diào)用Realize方法做初始化需曾。在不需要使用的時候調(diào)用Destroy方法釋放資源。

GetInterface

GetInterface可以說是OpenSL里使用頻率最高的方法,通過它我們可以獲取Object里面的Interface祈远。

由于一個Object里面可能包含了多個Interface,所以GetInterface方法有個SLInterfaceID參數(shù)來指定到的需要獲取Object里面的那個Interface呆万。

例如下面代碼我們通過EngineObject去獲取SL_IID_ENGINE這個id的Interface,而這個id對應(yīng)的Interface就是SLEngineItf:

//create EngineObject
SLObjectItf engineObject;
slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);

//get SLEngineItf
SLEngineItf engineInterface;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineInterface);

Interface

Interface則是方法的集合,例如SLRecordItf里面包含了和錄音相關(guān)的方法,SLPlayItf包含了和播放相關(guān)的方法。我們功能都是通過調(diào)用Interfaces的方法去實(shí)現(xiàn)的车份。

SLEngineItf

SLEngineItf是OpenSL里面最重要的一個Interface,我們可以通過它去創(chuàng)建各種Object,例如播放器谋减、錄音器、混音器的Object,然后在用這些Object去獲取各種Interface去實(shí)現(xiàn)各種功能扫沼。

struct SLEngineItf_ {
    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);

    SLresult (*CreateOutputMix) (SLEngineItf self, SLObjectItf * pMix, SLuint32 numInterfaces, const SLInterfaceID * pInterfaceIds, const SLboolean * pInterfaceRequired);

    ...
};

錄音

OpenSL的錄音功能是通過AudioRecorder來實(shí)現(xiàn)的,而AudioRecorder是通過SLEngineItf.CreateAudioRecorder方法創(chuàng)建的:

SLresult (*CreateAudioRecorder) (
        SLEngineItf self,
        SLObjectItf * pRecorder,
        SLDataSource * pAudioSrc,
        SLDataSink * pAudioSnk,
        SLuint32 numInterfaces,
        const SLInterfaceID * pInterfaceIds,
        const SLboolean * pInterfaceRequired
    );

各個參數(shù)的意義如下:

  • SLEngineItf C語言不像c++,沒有this指針,只能每次調(diào)用SLEngineItf的方法的時候手動傳入
  • SLObjectItf 用于保存創(chuàng)建出來的AudioRecorderObject
  • SLDataSource 數(shù)據(jù)的來源
  • SLDataSink 數(shù)據(jù)的去處
  • numInterfaces 與下面的SLInterfaceID和SLboolean配合使用,用于標(biāo)記SLInterfaceID數(shù)組和SLboolean的大小
  • SLInterfaceID 這里需要傳入一個數(shù)組,指定創(chuàng)建的AudioRecorderObject會包含哪些Interface
  • SLboolean 這里也是一個數(shù)組,用來標(biāo)記每個需要包含的Interface,如果AudioRecorderObject不支持,是不是需要直接創(chuàng)建AudioRecorderObject失敗出爹。

最后的三個參數(shù)用于指定AudioRecorderObject需要包含哪些Interface,如果不包含,是不是要直接創(chuàng)建失敗。如果成功的話我們就能使用AudioRecorderObject的GetInterface方法獲取到這些Interface了缎除。

SLDataSource和SLDataSink可能比較難理解严就。我們可以看下OpenSL錄音的原理:

2.png

簡而言之, AudioRecorder會從SLDataSource指定的數(shù)據(jù)源獲取數(shù)據(jù),然后將數(shù)據(jù)保存到SLDataSink指定的接收器。

SLDataSource很明顯就是錄音設(shè)備(SL_IODEVICE_AUDIOINPUT):

SLDataLocator_IODevice device;
device.locatorType = SL_DATALOCATOR_IODEVICE;
device.deviceType = SL_IODEVICE_AUDIOINPUT;
device.deviceID = SL_DEFAULTDEVICEID_AUDIOINPUT;
device.device = NULL; //Must be NULL if deviceID parameter is to be used.

SLDataSource source;
source.pLocator = &device;
source.pFormat = NULL; //This parameter is ignored if pLocator is SLDataLocator_IODevice.

而SLDataSink就可以任由我們指定了,它官方支持下面的類型:

SLDataLocator_Address
SLDataLocator_IODevice
SLDataLocator_OutputMix
SLDataLocator_URI
SLDataLocator_BufferQueue
SLDataLocator_MIDIBufferQueue

Android又拓展了下面幾種類型:

SLDataLocator_AndroidFD
SLDataLocator_AndroidBufferQueue
SLDataLocator_AndroidSimpleBufferQueue

我這邊把它設(shè)置成SLDataLocator_AndroidSimpleBufferQueue,它比較通用, AudioRecorder把數(shù)據(jù)放到這個隊(duì)列中,我們再可以從這個隊(duì)列中拿出來使用:

SLDataLocator_AndroidSimpleBufferQueue queue;
queue.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
queue.numBuffers = 2;

SLDataFormat_PCM format;
format.formatType = SL_DATAFORMAT_PCM;
format.numChannels = numChannels;
format.samplesPerSec = samplingRate;
format.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
format.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
format.channelMask = getChannelMask(numChannels);
format.endianness = SL_BYTEORDER_LITTLEENDIAN;

SLDataSink sink;
sink.pLocator = &queue;
sink.pFormat = &format;

同時在創(chuàng)建的時候需要檢測下SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE是不是支持:

SLInterfaceID id[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
SLboolean required[] = {SL_BOOLEAN_TRUE};

SLObjectItf recorderObject;
(engineInterface)->CreateAudioRecorder(
        engineInterface,
        &(recorderObject),
        &source,
        &sink,
        1,
        id,
        required
);
(*recorderObject)->Realize(recorderObject, SL_BOOLEAN_FALSE);

所以我們可以通過GetInterface獲取SLAndroidSimpleBufferQueueItf,然后注冊個隊(duì)列滿的監(jiān)聽回調(diào):

SLAndroidSimpleBufferQueueItf queueInterface;
(*recorderObject)->GetInterface(
        recorderObject,
        SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
        &(queueInterface)
);

(*queueInterface)->RegisterCallback(
        queueInterface,
        bufferQueueCallback,
        NULL
);

回調(diào)函數(shù)如下,我們可以在這個時候從隊(duì)列里面讀取下來的音頻數(shù)據(jù):

static void bufferQueueCallback(SLAndroidSimpleBufferQueueItf queue, void *pContext) {
    ...
}

最后需要打開錄音設(shè)備開始錄音:

SLRecordItf recorderInterface;
(*recorderObject)->GetInterface(
        recorderObject,
        SL_IID_RECORD,
        &(recorderInterface)
);

(*recorderInterface)->SetRecordState(
        recorderInterface,
        SL_RECORDSTATE_RECORDING
);

這里需要注意的是我們必須在隊(duì)列滿的時候?qū)?shù)據(jù)取出來,如果不取,那隊(duì)列里面就沒有空間可以繼續(xù)存儲音頻數(shù)據(jù)了:

(*queueInterface)->Enqueue(queueInterface, buffer, BUFFER_SIZE*sizeof(short));

播放

播放的代碼和錄音很類似器罐。我們需要先創(chuàng)建AudioPlayer:

SLresult (*CreateAudioPlayer) (
    SLEngineItf self,
    SLObjectItf * pPlayer,
    SLDataSource *pAudioSrc,
    SLDataSink *pAudioSnk,
    SLuint32 numInterfaces,
    const SLInterfaceID * pInterfaceIds,
    const SLboolean * pInterfaceRequired
);

它的參數(shù)和CreateAudioRecorder一樣,我就不再一個個去解釋了,可以看看播放的過程:

3.png

SLDataSource我也用SLDataLocator_AndroidSimpleBufferQueue,這樣我們可以往隊(duì)列中不斷寫入音頻數(shù)據(jù),AudioRecorder會從隊(duì)列中不斷獲取數(shù)據(jù)傳遞到混音器中:

SLDataLocator_AndroidSimpleBufferQueue queue;
queue.locatorType = SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE;
queue.numBuffers = 2;

SLDataFormat_PCM format;
format.formatType = SL_DATAFORMAT_PCM;
format.numChannels = numChannels;
format.samplesPerSec = samplingRate;
format.bitsPerSample = SL_PCMSAMPLEFORMAT_FIXED_16;
format.containerSize = SL_PCMSAMPLEFORMAT_FIXED_16;
format.channelMask = getChannelMask(numChannels);
format.endianness = SL_BYTEORDER_LITTLEENDIAN;

SLDataSource source;
source.pLocator = &queue;
source.pFormat = &format;

而SLDataSink需要配置成混音器梢为。混音器用于將多個音頻混合并且輸出到喇叭:

SLObjectItf outputMixObject;
(*engineInterface)->CreateOutputMix(
        engineInterface,
        &(outputMixObject),
        0,
        NULL,
        NULL
);
(*outputMixObject)->Realize(
        outputMixObject,
        SL_BOOLEAN_FALSE
);

SLDataLocator_OutputMix outputMix;
outputMix.locatorType = SL_DATALOCATOR_OUTPUTMIX;
outputMix.outputMix = outputMixObject;

SLDataSink sink;
sink.pLocator = &outputMix;
sink.pFormat = NULL; //This parameter is ignored if pLocator is SLDataLocator_IODevice or SLDataLocator_OutputMix.

同樣的我們在創(chuàng)建AudioPlayer的時候會檢查是不是支持SL_IID_ANDROIDSIMPLEBUFFERQUEUE:

SLObjectItf playerObject;
SLInterfaceID id[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE};
SLboolean required[] = {SL_BOOLEAN_TRUE};
(*engineInterface)->CreateAudioPlayer(
        engineInterface,
        &(playerObject),
        &source,
        &sink,
        1,
        id,
        required
);
(*playerObject)->Realize(playerObject, SL_BOOLEAN_FALSE);

最后我們需要注冊隊(duì)列空的監(jiān)聽和打開播放器開始播放:

SLAndroidSimpleBufferQueueItf queueInterface;
(*playerObject)->GetInterface(
        playerObject,
        SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
        &(queueInterface)
);
(*queueInterface)->RegisterCallback(
        queueInterface,
        bufferQueueCallback,
        NULL
);

//////Begin Playing//////
SLPlayItf playInterface;
(*playerObject)->GetInterface(
        playerObject,
        SL_IID_PLAY,
        &(playInterface)
);
(*playInterface)->SetPlayState(
        playInterface,
        SL_PLAYSTATE_PLAYING
);

Demo

這里有個簡單的錄音和播放的demo,按興趣的同學(xué)可以參考一下。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铸董,一起剝皮案震驚了整個濱河市祟印,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粟害,老刑警劉巖蕴忆,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異悲幅,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)夺艰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門郁副,熙熙樓的掌柜王于貴愁眉苦臉地迎上來存谎,“玉大人,你說我怎么就攤上這事既荚∏∑福” “怎么了?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵凿宾,是天一觀的道長初厚。 經(jīng)常有香客問我孙技,道長牵啦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任势似,我火速辦了婚禮,結(jié)果婚禮上障簿,老公的妹妹穿的比我還像新娘栅迄。我一直安慰自己,他們只是感情好西篓,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布岂津。 她就那樣靜靜地躺著吮成,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粱甫。 梳的紋絲不亂的頭發(fā)上茶宵,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天乌庶,我揣著相機(jī)與錄音契耿,去河邊找鬼宵喂。 笑死锅棕,一個胖子當(dāng)著我的面吹牛淌山,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播德绿,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蕴纳,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了古毛?” 一聲冷哼從身側(cè)響起稻薇,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤胶征,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后案狠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體暇昂,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡急波,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年名段,在試婚紗的時候發(fā)現(xiàn)自己被綠了泣懊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡信夫,死狀恐怖静稻,靈堂內(nèi)的尸體忽然破棺而出匈辱,到底是詐尸還是另有隱情亡脸,我是刑警寧澤树酪,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布续语,位于F島的核電站绵载,受9級特大地震影響娃豹,放射性物質(zhì)發(fā)生泄漏懂版。R本人自食惡果不足惜躏率,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一薇芝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嚷缭,春花似錦耍贾、人聲如沸荐开。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽佣渴。三九已至赫粥,卻和暖如春予借,著一層夾襖步出監(jiān)牢的瞬間频蛔,已是汗流浹背晦溪。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工三圆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留舟肉,地道東北人路媚。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓整慎,卻偏偏與公主長得像裤园,于是被迫代替她去往敵國和親拧揽。 傳聞我的和親對象是個殘疾皇子周循,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

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