使用OpenSL-ES播放PCM音頻文件
今天學(xué)習(xí)了使用OpenSL播放PCM文件,簡(jiǎn)單記錄一下。
感覺OpenSL入門的有些難度,搞得頭暈,所以只介紹功能性代碼,暫時(shí)不考慮健壯性,只抓學(xué)習(xí)重點(diǎn)。
學(xué)習(xí)OpenSL ES要先做好心理準(zhǔn)備,拿出時(shí)間認(rèn)真學(xué)習(xí),下一番功夫。
一、講在前面
在代碼之前先講一下原理,代碼講解和實(shí)例在第二節(jié)。懂了原理,那么在看代碼的時(shí)候才可能更容易理解。
1.1 OpenSL ES是什么?
OpenSL ES 全稱是:Open Sound Library for Embedded Systems,簡(jiǎn)單說(shuō)來(lái)OpenSL ES 是一套針對(duì)嵌入式平臺(tái)的音頻標(biāo)準(zhǔn)。
1.2 Android與OpenSL ES的關(guān)系
Android 2.3 (API 9) 即開始支持 OpenSL ES 標(biāo)準(zhǔn)了,通過 NDK 提供相應(yīng)的 API 開發(fā)接口,下圖是 Android 官方給出的關(guān)系圖:
由該圖可以看出,Android 實(shí)現(xiàn)的 OpenSL ES 只是 OpenSL 1.0.1 的子集,并且進(jìn)行了擴(kuò)展,因此,對(duì)于 OpenSL ES API 的使用,我們還需要特別留意哪些是 Android 支持的,哪些是不支持的,具體相關(guān)文檔的地址位于 NDK docs 目錄下:
NDKroot/docs/Additional_library_docs/opensles/index.html
NDKroot/docs/Additional_library_docs/opensles/OpenSL_ES_Specification_1.0.1.pdf
1.3 OpenSL ES的功能特點(diǎn)
支持以下特點(diǎn):
1)C 語(yǔ)言接口,兼容 C++,需要在 NDK 下開發(fā),能更好地集成在 native 應(yīng)用中
2)運(yùn)行于 native 層,需要自己管理資源的申請(qǐng)與釋放,沒有 Dalvik 虛擬機(jī)的垃圾回收機(jī)制
3)支持 PCM 數(shù)據(jù)的采集,支持的配置:16bit 位寬,16000 Hz采樣率,單通道。(其他的配置不能保證兼容所有平臺(tái))
4)支持 PCM 數(shù)據(jù)的播放,支持的配置:8bit/16bit 位寬,單通道/雙通道,小端模式,采樣率(8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000 Hz)
5)支持播放的音頻數(shù)據(jù)來(lái)源:res 文件夾下的音頻、assets 文件夾下的音頻、sdcard 目錄下的音頻、在線網(wǎng)絡(luò)音頻、代碼中定義的音頻二進(jìn)制數(shù)據(jù)等
不支持的:
不支持:
1)不支持版本低于 Android 2.3 (API 9) 的設(shè)備
2)沒有全部實(shí)現(xiàn) OpenSL ES 定義的特性和功能
3)不支持 MIDI
4)不支持直接播放 DRM 或者 加密的內(nèi)容
5)不支持音頻數(shù)據(jù)的編解碼,如需編解碼,需要使用 MediaCodec API 或者第三方庫(kù)
6)在音頻延時(shí)方面,相比于上層 API,并沒有特別明顯地改進(jìn)
優(yōu)勢(shì):
1)避免音頻數(shù)據(jù)頻繁在 native 層和 Java 層拷貝,提高效率
2)相比于 Java API,可以更靈活地控制參數(shù)
3)由于是 C 代碼,因此可以做深度優(yōu)化,比如采用 NEON 優(yōu)化
4)代碼細(xì)節(jié)更難被反編譯
1.4 OpenSL ES設(shè)計(jì)和概念
1.4.1 面向?qū)ο蟮?C 語(yǔ)言接口
OpenSL ES 雖然是 C 語(yǔ)言編寫,但是它的接口采用的是面向?qū)ο蟮姆绞?,并不是提供一系列的函?shù)接口,而是以 Interface 的方式來(lái)提供 API。
例如:
// 下面代碼是對(duì) Audio Engine 對(duì)象進(jìn)行 “初始化”
SLEngineItf engineObject;
SLresult result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
是不是很像C++的調(diào)用方式。
1.4.2 Objects 和 Interfaces
OpenSL ES 有兩個(gè)必須理解的概念,就是 Object 和 Interface,Object 可以想象成 Java 的 Object 類,Interface 可以想象成 Java 的 Interface,但它們并不完全相同,下面進(jìn)一步解釋他們的關(guān)系:
1) 每個(gè) Object 可能會(huì)存在一個(gè)或者多個(gè) Interface,官方為每一種 Object 都定義了一系列的 Interface
2)每個(gè) Object 對(duì)象都提供了一些最基礎(chǔ)的操作,比如:Realize,Resume,GetState,Destroy 等等,如果希望使用該對(duì)象支持的功能函數(shù),則必須通過其 GetInterface 函數(shù)拿到 Interface 接口,然后通過 Interface 來(lái)訪問功能函數(shù)
3)并不是每個(gè)系統(tǒng)上都實(shí)現(xiàn)了 OpenSL ES 為 Object 定義的所有 Interface,所以在獲取 Interface 的時(shí)候需要做一些選擇和判斷。
查看 OpenSLES.h
文件,我們可以看到 OpenSL ES 定義的所有 Object 對(duì)象的 ID,我們可以通過 Object ID 來(lái)創(chuàng)建對(duì)應(yīng)的對(duì)象實(shí)例,下面是一部分對(duì)象ID
/* Objects ID's */
#define SL_OBJECTID_ENGINE ((SLuint32) 0x00001001)
#define SL_OBJECTID_LEDDEVICE ((SLuint32) 0x00001002)
#define SL_OBJECTID_VIBRADEVICE ((SLuint32) 0x00001003)
#define SL_OBJECTID_AUDIOPLAYER ((SLuint32) 0x00001004)
#define SL_OBJECTID_AUDIORECORDER ((SLuint32) 0x00001005)
#define SL_OBJECTID_MIDIPLAYER ((SLuint32) 0x00001006)
#define SL_OBJECTID_LISTENER ((SLuint32) 0x00001007)
#define SL_OBJECTID_3DGROUP ((SLuint32) 0x00001008)
#define SL_OBJECTID_OUTPUTMIX ((SLuint32) 0x00001009)
#define SL_OBJECTID_METADATAEXTRACTOR ((SLuint32) 0x0000100A)
其中,我們比較常用的應(yīng)該就是:ENGINE、AUDIOPLAYER 和 AUDIORECORDER 對(duì)象了。
同樣,“OpenSLES.h” 文件中還定義了所有的 Interface ID,通過 Interface ID 我們可以從對(duì)象中獲取到對(duì)應(yīng)的功能接口。
例如:
extern SL_API const SLInterfaceID SL_IID_MIDITIME;
1.4.3 OpenSL ES的狀態(tài)機(jī)制
OpenSL ES的另外一個(gè)重要概念就是它的狀態(tài)機(jī)制:
任何一個(gè) OpenSL ES 的對(duì)象,創(chuàng)建成功后,都進(jìn)入 SL_OBJECT_STATE_UNREALIZED
狀態(tài),這種狀態(tài)下,系統(tǒng)不會(huì)為它分配任何資源,直到調(diào)用 Realize 函數(shù)為止。
Realize 后的對(duì)象,就會(huì)進(jìn)入 SL_OBJECT_STATE_REALIZED
狀態(tài),這是一種“可用”的狀態(tài),只有在這種狀態(tài)下,對(duì)象的各個(gè)功能和資源才能正常地訪問。
當(dāng)一些系統(tǒng)事件發(fā)生后,比如出現(xiàn)錯(cuò)誤或者 Audio 設(shè)備被其他應(yīng)用搶占,OpenSL ES 對(duì)象會(huì)進(jìn)入 SL_OBJECT_STATE_SUSPENDED
狀態(tài),如果希望恢復(fù)正常使用,需要調(diào)用 Resume 函數(shù)。
當(dāng)調(diào)用對(duì)象的 Destroy 函數(shù)后,則會(huì)釋放資源,并回到SL_OBJECT_STATE_UNREALIZED
狀態(tài)。
簡(jiǎn)言之,一個(gè) OpenSL ES 對(duì)象的生命周期,就是從 create 到 destroy 的過程,生命周期的控制,都是通過開發(fā)者顯示調(diào)用來(lái)完成的。
1.4.4 常用的對(duì)象和結(jié)構(gòu)體
在 OpenSL ES 中,一切 API 的訪問和控制都是通過 Interface 來(lái)完成的,連 OpenSL ES 里面的 Object 也是通過 SLObjectItf Interface 來(lái)訪問和使用的。
1) Engine 對(duì)象和SLEngineItf 接口
OpenSL ES 里面最核心的對(duì)象就是:Engine Object,音頻引擎對(duì)象,它主要提供如下幾個(gè)功能:
(1)管理 Audio Engine 的生命周期
(2)提供管理接口: SLEngineItf,該接口可以用來(lái)創(chuàng)建所有其他的 Object 對(duì)象
(3)提供設(shè)備屬性查詢接口:SLEngineCapabilitiesItf 和 SLAudioIODeviceCapabilitiesItf,這些接口可以查詢?cè)O(shè)備的一些屬性信息
Engine Object 對(duì)象的創(chuàng)建方法如下:
SLObjectItf engineObject;
slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );
初始化/銷毀:
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->Destroy(engineObject);
獲取管理接口:
SLEngineItf engineEngine;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(engineEngine));
2) Media Object
OpenSL ES 里面另一組比較重要的對(duì)象就是 Media Object ,代表著多媒體功能的抽象,比如:player、recorder 等等。
我們可以通過 SLEngineItf 提供的 CreateAudioPlayer
方法來(lái)創(chuàng)建一個(gè) player 對(duì)象實(shí)例,可以通過 SLEngineItf 提供的 CreateAudioRecorder
方法來(lái)創(chuàng)建一個(gè) recorder 實(shí)例。
3) Data Source 和 Data Sink
OpenSL ES 里面,這兩個(gè)結(jié)構(gòu)體均是作為創(chuàng)建 Media Object 對(duì)象時(shí)的參數(shù)而存在的。
- data source 代表著輸入源的信息,即數(shù)據(jù)從哪兒來(lái)、輸入的數(shù)據(jù)參數(shù)是怎樣的;
- data sink 則代表著輸出的信息,即數(shù)據(jù)輸出到哪兒、以什么樣的參數(shù)來(lái)輸出。
-
基本定義
DataSource 和DataSink定義如下:
typedef struct SLDataSource_ {
void *pLocator;
void *pFormat;
} SLDataSource;
typedef struct SLDataSink_ {
void *pLocator;
void *pFormat;
} SLDataSink;
可以看到這兩者的結(jié)構(gòu)體成員相同,都是一個(gè)locator和一個(gè)format,即資源定位器和資源格式。
Locator的格式定義了以下幾種 :
/** Data locator macros */
#define SL_DATALOCATOR_URI ((SLuint32) 0x00000001) //URI類型
#define SL_DATALOCATOR_ADDRESS ((SLuint32) 0x00000002) //
#define SL_DATALOCATOR_IODEVICE ((SLuint32) 0x00000003) //IO設(shè)備
#define SL_DATALOCATOR_OUTPUTMIX ((SLuint32) 0x00000004)
#define SL_DATALOCATOR_RESERVED5 ((SLuint32) 0x00000005)
#define SL_DATALOCATOR_BUFFERQUEUE ((SLuint32) 0x00000006)//緩沖區(qū)
#define SL_DATALOCATOR_MIDIBUFFERQUEUE ((SLuint32) 0x00000007)
#define SL_DATALOCATOR_RESERVED8 ((SLuint32) 0x00000008)
也就是說(shuō),Media Object 對(duì)象的輸入源/輸出源,既可以是 URL,也可以 Device,或者來(lái)自于緩沖區(qū)隊(duì)列等等,完全是由 Media Object 對(duì)象的具體類型和應(yīng)用場(chǎng)景來(lái)配置。
-
示例說(shuō)明
不同的 Media Object 對(duì)象實(shí)例,data source 和 data sink 的具體內(nèi)容是不一樣的。
對(duì)于Player而言:
image.png
而對(duì)于Recorder而言:
二、代碼流程講解
之前寫的一篇音視頻開發(fā)進(jìn)階指南(第四章)-AudioTrack播放PCM,相信大家都可以很容易看懂,因?yàn)镴ava的API非常清晰,方法命名和類型都很直觀,這就是OpenSL與AudioTrack學(xué)習(xí)起來(lái)的不同。
一、初始化播放器
先介紹兩個(gè)概念:創(chuàng)建接口,實(shí)例化。OpenSL里面的類型大體分成兩種SLObjectItf
和其它類型,前者稱為通用類型,其它的稱為具體類型。
- 通用類型
SLObjectItf
,這樣的需要?jiǎng)?chuàng)建接口并實(shí)例化,才能使用;因?yàn)槟悴恢浪木唧w類型。一般這種接口對(duì)象通過CreateXXX
函數(shù)來(lái)獲得 - 具體類型,例如
SLEngineItf
只需要?jiǎng)?chuàng)建接口就能使用,一般具體類型的接口對(duì)象通過GetInterface
,該函數(shù)需要傳入具體的類型ID。
創(chuàng)建接口過程
創(chuàng)建接口有兩種方法:
-
CreateXXX
,這種獲取的都是通用類型的接口,需要實(shí)例化 -
GetInterface
,獲取的是具體類型的接口,因?yàn)樗枰獋魅虢涌陬愋虸D,不需要實(shí)例化
實(shí)例化過程
實(shí)例化就是自己給自己實(shí)例化,所有類型的實(shí)例化是固定的方法:
//obj是通用類型
//第二個(gè)參數(shù)表示是否異步執(zhí)行 一般為false
(*obj)->Realize(obj, SL_BOOLEAN_FALSE);
播放的初始化工作是比較麻煩的,參數(shù)非常多,關(guān)鍵參數(shù)一定要弄清楚,否則不知其所以然。
1.1 引擎對(duì)象
想要調(diào)用OpenSL的API,它有一個(gè)唯一的門口slCreateEngine
,很多文章里叫它引擎,我就叫引擎門口,直觀一點(diǎn),門口里面還有其它的小門口。
SLObjectItf engineObj; //API門口
//1.1獲取引擎對(duì)象接口
SLresult result = slCreateEngine(&engineObj, 0, 0, 0, 0, 0);
//1.2 SLObjectItf 類型,需要實(shí)例化門口引擎對(duì)象接口
result = (*engineObj)->Realize(engineObj, SL_BOOLEAN_FALSE);
1.2 獲取引擎管理接口
有了引擎對(duì)象,接下來(lái)就要獲取需要的引擎管理接口了,OpenSL有多種引擎管理接口,通過ID區(qū)分,例如下面的SL_IID_ENGINE
SLEngineItf engineEngine;
//2.1獲取SLEngineItf類型引擎接口,后續(xù)操作將會(huì)使用這個(gè)接口
result = (*engineObj)->GetInterface(engineObj, SL_IID_ENGINE, &engineEngine);
//SLEngineItf 是具體類型不需要實(shí)例化
1.3 音頻混音
混音器用于將多個(gè)音頻混合并且輸出到喇叭
SLObjectItf outputMixObj;
const SLInterfaceID ids[] = {SL_IID_VOLUME};
const SLboolean req[] = {SL_BOOLEAN_FALSE};
//3.1創(chuàng)建音頻輸出混音對(duì)象接口
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObj, 0, ids, req);
//3.2 SLObjectItf 類型,實(shí)例化音頻輸出混音對(duì)象接口
result = (*outputMixObj)->Realize(outputMixObj, SL_BOOLEAN_FALSE);
//3.3 配置輸出管道
SLDataLocator_OutputMix outputMixLocator = {SL_DATALOCATOR_OUTPUTMIX, outputMixObj};
SLDataSink outputSink = {&outputMixLocator, NULL};
// 配置輸出源
//4.1配置緩沖區(qū)Buffer Queue參數(shù)
outputLocator = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
//4.2設(shè)置音頻源的音頻格式
SLDataFormat_PCM outputFormat = {
SL_DATAFORMAT_PCM, //指定PCM格式
2, //通道個(gè)數(shù)
SL_SAMPLINGRATE_44_1, //采樣率
SL_PCMSAMPLEFORMAT_FIXED_16,//采樣精度
SL_PCMSAMPLEFORMAT_FIXED_16,//窗口大小
SL_SPEAKER_FRONT_LEFT |
SL_SPEAKER_FRONT_RIGHT,//通道掩碼
SL_BYTEORDER_LITTLEENDIAN //字節(jié)序:小端
};
//4.3輸出源
SLDataSource outputSource = {&outputLocator, &outputFormat};
1.4 獲取播放器對(duì)象門口
播放器門口不是具體執(zhí)行播放的工具,而是管理播放相關(guān)的緩沖,音頻格式,混音,輸出等
//5.1獲取播放器對(duì)象接口
SLObjectItf audioPlayerObj;
const SLInterfaceID outputInterfaces[1] = {SL_IID_BUFFERQUEUE};
const SLboolean requireds[2] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine,
&audioPlayerObj,
&outputSource,//輸出源
&outputSink,//輸出管道
1,//接口個(gè)數(shù)
outputInterfaces,//輸出接口
requireds); //接口配置
//看到了沒,又是SLObjectItf 類型,還得實(shí)例化
//5.2實(shí)例化播放器對(duì)象接口
result = (*audioPlayerObj)->Realize(audioPlayerObj, SL_BOOLEAN_FALSE);
1.5 音頻輸出對(duì)象
音頻輸出對(duì)象就是音頻數(shù)據(jù)本身,具體一點(diǎn)就是存放即將被播放的數(shù)據(jù)所在的緩沖區(qū)
//6.1獲取具體音頻輸出對(duì)象接口
SLAndroidSimpleBufferQueueItf outputBufferQueueInterface;
result = (*audioPlayerObj)->GetInterface(audioPlayerObj, SL_IID_ANDROIDSIMPLEBUFFERQUEUE,
&outputBufferQueueInterface);
//SLAndroidSimpleBufferQueueItf 是具體類型,不用實(shí)例化
1.6 具體的播放器對(duì)象
它是用來(lái)執(zhí)行播放功能的,其它的條件都給它準(zhǔn)備好了
SLPlayItf audioPlayerPlay;
//7.1獲取播放器播放對(duì)象接口
result = (*audioPlayerObj)->GetInterface(audioPlayerObj, SL_IID_PLAY, &audioPlayerPlay);
if (result != SL_RESULT_SUCCESS) {
LOGD("audioPlayerObj SL_IID_PLAY GetInterface failed,result=%d", result);
return result;
}
//具體類型,不用實(shí)例化
1.7 設(shè)置回調(diào)
回調(diào)函數(shù)的作用是:通知。
通知什么?在播放的時(shí)候,OpenSL不會(huì)一次性把所有數(shù)據(jù)都讀到緩沖區(qū),需要用一點(diǎn),拷貝一點(diǎn),這個(gè)函數(shù)就是播放器告訴你,緩存用光了,需要新的數(shù)據(jù)。
所以在回調(diào)函數(shù)中需要把新的數(shù)據(jù)拷貝到緩沖區(qū)。
//8.1設(shè)置回調(diào)
result = (*outputBufferQueueInterface)->RegisterCallback(outputBufferQueueInterface,
PlayCallback,
this);
二、開始播放
//9設(shè)置為播放狀態(tài)
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_PLAYING);
LOGI("setPlayerState:SL_PLAYSTATE_PLAYING");
//10啟動(dòng)回調(diào)機(jī)制,開始播放
PlayCallback(outputBufferQueueInterface, this);
三、寫數(shù)據(jù)
前面說(shuō)了,回調(diào)函數(shù)中需要填充新的數(shù)據(jù):
SLuint32 getPcmData(void **pcm, FILE *pcmFile, uint8_t *out_buffer) {
while (!feof(pcmFile)) {
//因?yàn)镻CM采樣率為44100,采樣精度為16BIT,所以一次讀取2秒鐘的采樣
size_t size = fread(out_buffer, 1, 44100 * 2 * 2, pcmFile);
*pcm = out_buffer;
return size;
}
return 0;
}
//當(dāng)outputBufferQueueInterface中的數(shù)據(jù)消耗完就會(huì)觸發(fā)回調(diào)
void PlayCallback(SLAndroidSimpleBufferQueueItf bufferQueue, void *pContext) {
LOGI("PlayCallback");
//獲取數(shù)據(jù)
SLuint32 size = getPcmData(&readPCMBuffer, pcmFile, tempBuffer);
LOGI("PlayCallback, size=%d", size);
if (NULL != readPCMBuffer && size > 0) {
SLresult result = (*outputBufferQueueInterface)->Enqueue(outputBufferQueueInterface,
readPCMBuffer, size);
}
}
停止播放
//11.停止播放
(*audioPlayerPlay)->SetPlayState(audioPlayerPlay, SL_PLAYSTATE_STOPPED);
釋放OpenSL ES資源
只需要銷毀OpenSL ES對(duì)象,接口不需要做Destroy處理
(*engineObj)->Destroy(engineObj);
(*outputMixObj)->Destroy(outputMixObj);
(*audioPlayerObj)->Destroy(audioPlayerObj);
源碼Demo地址github
參考
Android音頻開發(fā)(6):使用 OpenSL ES API(上)
Android音頻開發(fā)(7):使用 OpenSL ES API(下)