Android音頻開發(fā)之OpenSL ES

開發(fā)Android上的音頻應(yīng)用稠项,最常見的是使用MediaRecorderMediaPlayer來實(shí)現(xiàn)音頻的錄制和播放荞驴,更基礎(chǔ)點(diǎn)的會(huì)使用AudioRecordAudioTrack來實(shí)現(xiàn)烦粒。用這兩種方式已經(jīng)能應(yīng)對(duì)絕大部分的音頻開發(fā)需求了结窘。更底層的API错负,如NDK層的OpenSL ES則鮮有問津项贺。

最近因?yàn)楣ぷ餍枰佑|了NDK層相關(guān)API寇蚊,這里簡(jiǎn)要記錄下OpenSL ES相關(guān)的知識(shí)笔时。

關(guān)于OpenSL ES

HelloWorld

不同于傳統(tǒng)的HelloWorld程序,這個(gè)示例稍微復(fù)雜一點(diǎn)仗岸,而且這回我們的實(shí)現(xiàn)允耿,將讓我們聽到這句經(jīng)典的編程入門歡迎語。

實(shí)現(xiàn)思路

此處應(yīng)有圖扒怖,一圖頂萬言
  1. 創(chuàng)建并初始化Audio Engine(音頻引擎较锡,是和底層交互的入口)
  2. 打開OutputMix(音頻輸出),配置相關(guān)參數(shù)盗痒、Buffer Queue(緩沖隊(duì)列)蚂蕴,以便進(jìn)行音頻播放
  3. 打開Audio Input(音頻輸入),配置相關(guān)參數(shù),配置Buffer Queue骡楼,以便獲取音頻輸入
  4. 設(shè)置輸出熔号、輸入的Callback(回調(diào)函數(shù)),實(shí)現(xiàn)將輸入傳給輸出的邏輯
  5. 啟動(dòng)音頻錄制

也就是鸟整,這個(gè)程序?qū)崿F(xiàn)的功能是:將話筒錄制的聲音跨嘉,再播放出來,也就是返聽的效果吃嘿。

代碼實(shí)現(xiàn)

1. 創(chuàng)建并初始化Audio Engine

// 創(chuàng)建Audio Engine
result = slCreateEngine(&openSLEngine, 0, NULL, 0, NULL, NULL);

// 初始化上一步得到的openSLEngine
result = (*openSLEngine)->Realize(openSLEngine, SL_BOOLEAN_FALSE);

// 獲取SLEngine接口對(duì)象祠乃,后續(xù)的操作將使用這個(gè)對(duì)象
SLEngineItf openSLEngineInterface = NULL;
result = (*openSLEngine)->GetInterface(openSLEngine, SL_IID_ENGINE, &openSLEngineInterface);

2. 音頻輸出

2.1 打開音頻輸出設(shè)備

// 相關(guān)參數(shù)
const SLInterfaceID ids[] = {SL_IID_VOLUME};
const SLboolean req[] = {SL_BOOLEAN_FALSE};

// 使用第一步的openSLEngineInterface,創(chuàng)建音頻輸出Output Mix
result = (*openSLEngineInterface)->CreateOutputMix(openSLEngineInterface, &outputMix, 0, ids, req);

// 初始化outputMix
result = (*outputMix)->Realize(outputMix, SL_BOOLEAN_FALSE);

// 由于不需要操作到ouputMix兑燥,所以這一步就不去獲取它的接口對(duì)象

2.2 配置相關(guān)參數(shù)

// Buffer Queue的參數(shù)
SLDataLocator_AndroidSimpleBufferQueue outputLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };

// 設(shè)置音頻格式
SLDataFormat_PCM outputFormat = { SL_DATAFORMAT_PCM, 2, samplerate, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, SL_BYTEORDER_LITTLEENDIAN };

// 輸出源
SLDataSource outputSource = { &outputLocator, &outputFormat };

// 輸出管道
SLDataLocator_OutputMix outputMixLocator = { SL_DATALOCATOR_OUTPUTMIX, outputMix };
SLDataSink outputSink = { &outputMixLocator, NULL };

2.3 創(chuàng)建播放器

// 參數(shù)
const SLInterfaceID outputInterfaces[1] = { SL_IID_BUFFERQUEUE };
const SLboolean requireds[2] = { SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE };

// 創(chuàng)建音頻播放對(duì)象AudioPlayer
result = (*openSLEngineInterface)->CreateAudioPlayer(openSLEngineInterface, &audioPlayerObject, &outputSource, &outputSink, 1, outputInterfaces, requireds);

// 初始化AudioPlayer
result = (*audioPlayerObject)->Realize(audioPlayerObject, SL_BOOLEAN_FALSE);

// 獲取音頻輸出的BufferQueue接口
SLAndroidSimpleBufferQueueItf outputBufferQueueInterface = NULL;
result = (*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &outputBufferQueueInterface); 

// 獲取播放器接口
SLPlayItf outputPlayInterface;
result = (*audioPlayerObject)->GetInterface(audioPlayerObject, SL_IID_PLAY, &audioPlayerInterface);

3. 音頻輸入

3.1 配置參數(shù)

// 參數(shù)
SLDataLocator_IODevice deviceInputLocator = { SL_DATALOCATOR_IODEVICE, SL_IODEVICE_AUDIOINPUT, SL_DEFAULTDEVICEID_AUDIOINPUT, NULL };
SLDataSource inputSource = { &deviceInputLocator, NULL };

SLDataLocator_AndroidSimpleBufferQueue inputLocator = { SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 1 };
SLDataFormat_PCM inputFormat = { SL_DATAFORMAT_PCM, 2, samplerate * 1000, SL_PCMSAMPLEFORMAT_FIXED_16, SL_PCMSAMPLEFORMAT_FIXED_16, SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, 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_FALSE };

3.2 創(chuàng)建錄制器

// 創(chuàng)建AudioRecorder
result = (*openSLEngineInterface)->CreateAudioRecorder(openSLEngineInterface, &andioRecorderObject, &inputSource, &inputSink, 2, inputInterfaces, requireds);

// 初始化AudioRecorder
result = (*andioRecorderObject)->Realize(andioRecorderObject, SL_BOOLEAN_FALSE);

// 獲取音頻輸入的BufferQueue接口
result = (*andioRecorderObject)->GetInterface(andioRecorderObject, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, &inputBufferQueueInterface);

// 獲取錄制器接口
SLRecordItf audioRecorderInterface;
(*andioRecorderObject)->GetInterface(andioRecorderObject, SL_IID_RECORD, &audioRecorderInterface);

4. 配置回調(diào)并啟動(dòng)輸入亮瓷、輸出

4.1 配置輸入、輸出回調(diào)

// 輸出回調(diào)
result = *outputBufferQueueInterface)->RegisterCallback(outputBufferQueueInterface, outputCallback, NULL);

// 輸入回調(diào)
result = (*inputBufferQueueInterface)->RegisterCallback(inputBufferQueueInterface, inputCallback, NULL);

4.2 啟動(dòng)輸入輸出

// 設(shè)置為播放狀態(tài)
result = (*audioPlayerInterface)->SetPlayState(audioPlayerInterface, SL_PLAYSTATE_PLAYING);
// 設(shè)為錄制狀態(tài)
result = (*andioRecorderObject)->SetRecordState(andioRecorderObject, SL_RECORDSTATE_RECORDING);

// 啟動(dòng)回調(diào)機(jī)制
(*inputBufferQueueInterface)->Enqueue(inputBufferQueueInterface, inputBuffers[0], buffersize * 4);
(*outputBufferQueueInterface)->Enqueue(outputBufferQueueInterface, outputBuffers[0], buffersize * 4);

4.3 回調(diào)函數(shù)的實(shí)現(xiàn)

// 音頻輸入回調(diào)
static void inputCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
    // 獲取同步鎖
    pthread_mutex_lock(&mutex);
    
    // 取一個(gè)可用的緩存
    short int *inputBuffer = inputBuffers[inputBufferWrite];
    if (inputBuffersAvailable == 0) 
        inputBufferRead = inputBufferWrite;
    // 可用緩存+1
    inputBuffersAvailable++;
    if (inputBufferWrite < numBuffers - 1) 
        inputBufferWrite++; 
    else
        inputBufferWrite = 0;

    pthread_mutex_unlock(&mutex);
    
    // 調(diào)用BufferQueue的Enqueue方法降瞳,把輸入數(shù)據(jù)取到inputBuffer
    (*bufferQueue)->Enqueue(bufferQueue, inputBuffer, buffersize * 4);
}

// 音頻輸出回調(diào)
static void outputCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
    short int *outputBuffer = outputBuffers[outputBufferIndex];
    pthread_mutex_lock(&mutex);

    if (inputBuffersAvailable < 1) {
        pthread_mutex_unlock(&mutex);
        memset(outputBuffer, 0, buffersize * 4);
    } else {
        short int *inputBuffer = inputBuffers[inputBufferRead];
        if (inputBufferRead < numBuffers - 1) inputBufferRead++; else inputBufferRead = 0;
        inputBuffersAvailable--;
        pthread_mutex_unlock(&mutex);
        memcpy(outputBuffer, inputBuffer, buffersize * 4);
    }

    (*bufferQueue)->Enqueue(bufferQueue, outputBuffer, buffersize * 4);

    if (outputBufferIndex < numBuffers - 1) outputBufferIndex++; else outputBufferIndex = 0;
}

回調(diào)函數(shù)使用生產(chǎn)者-消費(fèi)者機(jī)制實(shí)現(xiàn)嘱支,當(dāng)輸入可用的時(shí)候,就從輸入的緩沖隊(duì)列里取數(shù)據(jù)出來挣饥,放到inputBuffers里除师;然后當(dāng)輸出準(zhǔn)備就緒的時(shí)候,再?gòu)膇nputBuffers里取出數(shù)據(jù)扔枫,復(fù)制一份汛聚,然后放入輸出的緩沖隊(duì)列里。就這樣實(shí)現(xiàn)了把音頻輸出轉(zhuǎn)到音頻輸出的效果短荐。

關(guān)于OpenSL的使用

使用OpenSL相關(guān)API的通用步驟是:

  1. 創(chuàng)建對(duì)象(通過帶有create的函數(shù))
  2. 初始化(通過Realize函數(shù))
  3. 獲取接口來使用相關(guān)功能(通過GetInterface函數(shù))

OpenSL使用回調(diào)機(jī)制來訪問音頻IO倚舀,但不像跟Jack、CoreAudio那些音頻異步IO框架忍宋,OpenSL 的回調(diào)里并不會(huì)把音頻數(shù)據(jù)作為參數(shù)傳遞痕貌,回調(diào)方法僅僅是告訴我們:BufferQueue已經(jīng)就緒,可以接受/獲取數(shù)據(jù)了糠排。

使用SLBufferQueueItf. Enqueue函數(shù)從(往)音頻設(shè)備獲取(放入)數(shù)據(jù)舵稠。完整的函數(shù)簽名是:SLresult (*Enqueue) (SLBufferQueueItf self, const void *pBuffer, SLuint32 size);

當(dāng)BufferQueue就緒,這個(gè)方法就應(yīng)被調(diào)用入宦。當(dāng)開啟錄制或開始播放時(shí)哺徊,BufferQueue就可以接受數(shù)據(jù)。這之后云石,回調(diào)機(jī)制通過回調(diào)來告知應(yīng)用程序它已經(jīng)準(zhǔn)備好唉工,可以消費(fèi)(提供)數(shù)據(jù)研乒。

Enqueue方法可以在回調(diào)里調(diào)用汹忠,可以不。

如果選擇在回調(diào)里調(diào)用,那么在開始播放(錄制)的時(shí)候宽菜,需要先調(diào)用Enqueue來啟動(dòng)回調(diào)機(jī)制谣膳,否則回調(diào)將不會(huì)被調(diào)用到。

如果選擇不在回調(diào)里調(diào)用铅乡,回調(diào)則用于通知程序继谚,它準(zhǔn)備就緒了。程序可以在得到足夠的數(shù)據(jù)緩存之后阵幸,再把數(shù)據(jù)給它處理花履。這示例使用的是前一種方式,在回調(diào)里調(diào)用Enqueue挚赊。

That's all

使用OpenSL ES可以更高效的使用Android的音頻系統(tǒng)诡壁,尤其是需要低延遲的場(chǎng)景,如返聽荠割。隨著Android設(shè)備性能的提升妹卿,以及Android系統(tǒng)的不斷優(yōu)化,音頻延遲的問題已經(jīng)有了可觀的性能提升蔑鹦。而在游戲領(lǐng)域夺克,OpenSL ES的高性能也能提供更棒的游戲體驗(yàn),甚至讓移動(dòng)平臺(tái)也有打造《吟誦者(In Verbis Virtus)》這種語音類游戲的可能嚎朽。

But not ALL

對(duì)于OpenSL ES也僅僅是草草接觸铺纽,如有不對(duì)或疏漏的地方,還請(qǐng)大家指正哟忍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末室囊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子魁索,更是在濱河造成了極大的恐慌融撞,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粗蔚,死亡現(xiàn)場(chǎng)離奇詭異尝偎,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)鹏控,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門致扯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人当辐,你說我怎么就攤上這事抖僵。” “怎么了缘揪?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵耍群,是天一觀的道長(zhǎng)义桂。 經(jīng)常有香客問我,道長(zhǎng)蹈垢,這世上最難降的妖魔是什么慷吊? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮曹抬,結(jié)果婚禮上溉瓶,老公的妹妹穿的比我還像新娘。我一直安慰自己谤民,他們只是感情好堰酿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著张足,像睡著了一般胞锰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上兢榨,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天嗅榕,我揣著相機(jī)與錄音,去河邊找鬼吵聪。 笑死凌那,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吟逝。 我是一名探鬼主播帽蝶,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼块攒!你這毒婦竟也來了励稳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤囱井,失蹤者是張志新(化名)和其女友劉穎驹尼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庞呕,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡新翎,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了住练。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片地啰。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖讲逛,靈堂內(nèi)的尸體忽然破棺而出亏吝,到底是詐尸還是另有隱情,我是刑警寧澤盏混,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布蔚鸥,位于F島的核電站惜论,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏株茶。R本人自食惡果不足惜来涨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一图焰、第九天 我趴在偏房一處隱蔽的房頂上張望启盛。 院中可真熱鬧,春花似錦技羔、人聲如沸僵闯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鳖粟。三九已至,卻和暖如春拙绊,著一層夾襖步出監(jiān)牢的瞬間向图,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工标沪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榄攀,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓金句,卻偏偏與公主長(zhǎng)得像檩赢,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子违寞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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