目錄
- OpenSL ES基本介紹
- OpenSL ES播放音頻流程
- 代碼實現(xiàn)
- 遇到的問題
- 資料
- 收獲
上一篇我們通過AudioTrack實現(xiàn)了FFmpeg解碼后的PCM音頻數(shù)據(jù)的播放窟勃,在Android上還有一種播放音頻的方式即OpenSL ES, 什么是OpenSL ES哥艇,這個我們平時接觸的很少,原因是平時業(yè)務中大部分播放可以通過Java層的MediaPlayer或者AudioTrack實現(xiàn)音頻播放。如果遇到一些特殊的需求祠挫,比如添加音效等這是不容易實現(xiàn)。而OpenSL可以很好的解決此類問題,并且還有很多豐富的功能。下面我們一起來學習實踐吧周偎。
一、OpenSL ES基本介紹
1.1 OPenSL ES 是什么撑帖?
OpenSL ES (Open Sound Library for Embedded System) ,即嵌入式音頻加速標準與 Android Java 框架中的 MediaPlayer 和 MediaRecorderAPI 提供類似的音頻功能蓉坎。OpenSL ES 提供 C 語言接口和 CPP 綁定,讓您可以從使用任意一種語言編寫的代碼中調用 API胡嘿。
相對MediaPlayer 和 MediaRecorderAPI 等java層API來說蛉艾,OpenSL ES 則是比價低層級的 API, 屬于 C 語言 API 。在開發(fā)中衷敌,一般會直接使用高級 API , 除非遇到性能瓶頸勿侯,如語音實時聊天、3D Audio 逢享、某些 Effects 等,開發(fā)者可以直接通過 C/CPP開發(fā)基于 OpenSL ES 音頻的應用, 提升應用的音頻性能吴藻。
1.2 OpenSL ES有哪些能力吶瞒爬?
我們通過下圖的OpenSL ES使用指南中可以看到支持,音頻的播放沟堡、混音侧但、音效、以及錄制等功能航罗。
上述兩種圖片來自:官方指南:OpenSL ES
1.3 如何引入禀横?
OpenSL ES的庫我們可以在NDK 軟件包中找到
eg: $NDK_PATH_/platforms/android-30/arch-arm/usr/lib/libOpenSLES.so
引入方式只需要在CmakeList.txt的target_link_libraries中加入OpenSLES即可
target_link_libraries(
native-lib
avformat
avcodec
avfilter
avutil
swresample
swscale
OpenSLES
${log-lib})
1.4 對象與接口
OpenES SL雖然是面向過程的C語言編寫的,但是以面向對象的思想提供了對象和接口粥血,方便開發(fā)的在項目中使用柏锄。
OpenSL ES 對象類似于 Java 和 CPP 等編程語言中的對象概念酿箭,不過 OpenSL ES 對象僅能通過其關聯(lián)接口進行訪問。其中包括所有對象的初始接口趾娃,稱為 SLObjectItf缭嫡。對象本身沒有句柄,只有一個連接到對象的 SLObjectItf 接口的句柄抬闷。
需要注意的是 OpenSL ES 對象不能直接使用妇蛀,必須通過其 GetInterface 函數(shù)用ID號拿到指定接口(如播放器的播放接口),然后通過該接口來訪問功能函數(shù)
OpenSL ES 對象是先創(chuàng)建的笤成,它會返回 SLObjectItf评架,然后再實現(xiàn) (realize),然后使用 GetInterface,為其需要的每種功能獲取接口
音頻播放會用到 引擎炕泳、混音器以及播放器對象和接口纵诞,下一小節(jié)我們來看下具體流程。
二喊崖、OpenSL ES播放音頻流程
圖片來源: OpenSL-ES 官方文檔
在CmakeList引入OpenSL庫挣磨,然后在對應的CPP文件中導入相應的頭文件即可使用OpenSL ES,具體流程如下
- 創(chuàng)建引擎對象
SLObjectItf engineObj
初始化引擎Realize
獲取引擎接口GetInterface SLEngineItf
- 創(chuàng)建混音器對象
SLObjectItf outputMixObj
初始化混音器Realize
- 設置輸入輸出數(shù)據(jù)參數(shù)
- 創(chuàng)建播放器對象
SLPlayItf playerObj
初始化播放器Realize
獲取播放器接口GetInterface
- 獲取播放回調接口(即緩沖隊列)
SLAndroidSimpleBufferQueueItf bufferQueue
- 注冊播放回調 `RegisterCallback
- 設置播放狀態(tài)
SetPlayState
- 等待音頻幀加入隊列觸發(fā)播放回調
(*mBufferQueue)->Enqueue
- 釋放資源
具體參考官方提供的示例demo native-audio 是一個簡單的音頻錄制器/播放器
三荤懂、OpenSL ES播放解碼PCM的代碼實現(xiàn)
了解了OpenSL ES的基本知識和使用流程茁裙,下面我們開始具體的代碼實現(xiàn)。
#include <jni.h>
#include <string>
#include <unistd.h>
extern "C" {
#include "include/libavcodec/avcodec.h"
#include "include/libavformat/avformat.h"
#include "include/log.h"
#include <libswscale/swscale.h>
#include <libavutil/imgutils.h>
#include <libswresample/swresample.h>
#include <SLES/OpenSLES.h>
#include <SLES/OpenSLES_Android.h>
}
//函數(shù)聲明
jint playPcmBySL(JNIEnv *env, jstring pcm_path);
extern "C"
JNIEXPORT jint JNICALL
Java_android_spport_mylibrary2_Demo_decodeAudio(JNIEnv *env, jobject thiz, jstring video_path,
jstring pcm_path) {
....
//在音頻解碼完成后調用使用sl播放的函數(shù)
playPcmBySL(env,pcm_path);
}
// engine interfaces
static SLObjectItf engineObject = NULL;
static SLEngineItf engineEngine;
// output mix interfaces
static SLObjectItf outputMixObject = NULL;
static SLEnvironmentalReverbItf outputMixEnvironmentalReverb = NULL;
static SLObjectItf pcmPlayerObject = NULL;
static SLPlayItf pcmPlayerPlay;
static SLAndroidSimpleBufferQueueItf pcmBufferQueue;
FILE *pcmFile;
void *buffer;
uint8_t *out_buffer;
jint playPcmBySL(JNIEnv *env, const _jstring *pcm_path);
// aux effect on the output mix, used by the buffer queue player
static const SLEnvironmentalReverbSettings reverbSettings = SL_I3DL2_ENVIRONMENT_PRESET_STONECORRIDOR;
//播放回調
void playerCallback(SLAndroidSimpleBufferQueueItf bufferQueueItf, void *context) {
if (bufferQueueItf != pcmBufferQueue) {
LOGE("SLAndroidSimpleBufferQueueItf is not equal");
return;
}
while (!feof(pcmFile)) {
size_t size = fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);
if (out_buffer == NULL || size == 0) {
LOGI("read end %ld", size);
} else {
LOGI("reading %ld", size);
}
buffer = out_buffer;
break;
}
if (buffer != NULL) {
LOGI("buffer is not null");
SLresult result = (*pcmBufferQueue)->Enqueue(pcmBufferQueue, buffer, 44100 * 2 * 2);
if (SL_RESULT_SUCCESS != result) {
LOGE("pcmBufferQueue error %d",result);
}
}
}
jint playPcmBySL(JNIEnv *env, jstring pcm_path) {
const char *pcmPath = env->GetStringUTFChars(pcm_path, NULL);
pcmFile = fopen(pcmPath, "r");
if (pcmFile == NULL) {
LOGE("open pcmfile error");
return -1;
}
out_buffer = (uint8_t *) malloc(44100 * 2 * 2);
//1. 創(chuàng)建引擎`
// SLresult result;
//1.1 創(chuàng)建引擎對象
SLresult result = slCreateEngine(&engineObject, 0, 0, 0, 0, 0);
if (SL_RESULT_SUCCESS != result) {
LOGE("slCreateEngine error %d", result);
return -1;
}
//1.2 實例化引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
LOGE("Realize engineObject error");
return -1;
}
//1.3獲取引擎接口SLEngineItf
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
if (SL_RESULT_SUCCESS != result) {
LOGE("GetInterface SLEngineItf error");
return -1;
}
slCreateEngine(&engineObject, 0, 0, 0, 0, 0);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
//獲取到SLEngineItf接口后节仿,后續(xù)的混音器和播放器的創(chuàng)建都會使用它
//2. 創(chuàng)建輸出混音器
const SLInterfaceID ids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean req[1] = {SL_BOOLEAN_FALSE};
//2.1 創(chuàng)建混音器對象
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids, req);
if (SL_RESULT_SUCCESS != result) {
LOGE("CreateOutputMix error");
return -1;
}
//2.2 實例化混音器
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
LOGE("outputMixObject Realize error");
return -1;
}
//2.3 獲取混音接口 SLEnvironmentalReverbItf
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
&outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
outputMixEnvironmentalReverb, &reverbSettings);
}
//3 設置輸入輸出數(shù)據(jù)源
//setSLData();
//3.1 設置輸入 SLDataSource
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
SLDataFormat_PCM formatPcm = {
SL_DATAFORMAT_PCM,//播放pcm格式的數(shù)據(jù)
2,//2個聲道(立體聲)
SL_SAMPLINGRATE_44_1,//44100hz的頻率
SL_PCMSAMPLEFORMAT_FIXED_16,//位數(shù) 16位
SL_PCMSAMPLEFORMAT_FIXED_16,//和位數(shù)一致就行
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,//立體聲(前左前右)
SL_BYTEORDER_LITTLEENDIAN//結束標志
};
SLDataSource slDataSource = {&loc_bufq, &formatPcm};
//3.2 設置輸出 SLDataSink
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
SLDataSink audioSnk = {&loc_outmix, NULL};
//4.創(chuàng)建音頻播放器
//4.1 創(chuàng)建音頻播放器對象
const SLInterfaceID ids2[1] = {SL_IID_BUFFERQUEUE};
const SLboolean req2[1] = {SL_BOOLEAN_TRUE};
result = (*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slDataSource, &audioSnk,
1, ids2, req2);
if (SL_RESULT_SUCCESS != result) {
LOGE(" CreateAudioPlayer error");
return -1;
}
//4.2 實例化音頻播放器對象
result = (*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);
if (SL_RESULT_SUCCESS != result) {
LOGE(" pcmPlayerObject Realize error");
return -1;
}
//4.3 獲取音頻播放器接口
result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &pcmPlayerPlay);
if (SL_RESULT_SUCCESS != result) {
LOGE(" SLPlayItf GetInterface error");
return -1;
}
//5. 注冊播放器buffer回調 RegisterCallback
//5.1 獲取音頻播放的buffer接口 SLAndroidSimpleBufferQueueItf
result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
if (SL_RESULT_SUCCESS != result) {
LOGE(" SLAndroidSimpleBufferQueueItf GetInterface error");
return -1;
}
//5.2 注冊回調 RegisterCallback
result = (*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, playerCallback, NULL);
if (SL_RESULT_SUCCESS != result) {
LOGE(" SLAndroidSimpleBufferQueueItf RegisterCallback error");
return -1;
}
//6. 設置播放狀態(tài)為Playing
result = (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);
if (SL_RESULT_SUCCESS != result) {
LOGE(" SetPlayState error");
return -1;
}
//7.觸發(fā)回調
playerCallback(pcmBufferQueue,NULL);
return 0;
}
OpenSL ES 還有更多豐富的功能晤锥,比如,混音廊宪、設置音量矾瘾、錄音、播放url或者assert中的音頻箭启。詳細了解可以查看官方文檔和NDK的demo壕翩,
本篇就學習實踐到這里,越學習發(fā)下身邊優(yōu)秀的人越多傅寡,自己不會的東西放妈、要學習的就越多,抓住一個核心痛點荐操,一起學習實踐吧芜抒。
代碼已上傳至github。[https://github.com/ayyb1988/ffmpegvideodecodedemo] 歡迎交流托启,一起學習成長宅倒。
四、遇到的問題
問題1: 拿到混音接口對象后沒有SetEnvironmentalReverbProperties設置后result不為0導致家了為0判斷屯耸,導致這里一直提示出錯拐迁。
解決方案蹭劈,去掉此處的result檢查,官方的demo也返回一樣的值16
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
&outputMixEnvironmentalReverb);
if (SL_RESULT_SUCCESS == result) {
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
outputMixEnvironmentalReverb, &reverbSettings);
if (SL_RESULT_SUCCESS != result) {
LOGE(" SetEnvironmentalReverbProperties error");
return -1;
}
}
改為如下:
result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
outputMixEnvironmentalReverb, &reverbSettings);
問題2: 創(chuàng)建播放器對象一直為空唠亚,導致無法播放
原因:給SLData 設置數(shù)據(jù)源時
SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE錯誤的寫成了SL_DATALOCATOR_ANDROIDBUFFERQUEUE
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDBUFFERQUEUE, 2};
-->改為
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE,2};
問題3. 播放音頻時音頻卡住不斷重復
while (!feof(pcmFile)) {
size_t size = fread(out_buffer, 44100 * 2 * 2, 1, pcmFile);
if (out_buffer == NULL || size == 0) {
LOGI("read end %ld", size);
} else {
LOGI("reading %ld", size);
}
buffer = out_buffer;
//原因是链方,忘記跳出循環(huán)了
break;
}
在學習的初期一個小錯誤就可能折騰幾個小時,在采用逐步排查流程和查看細節(jié)灶搜、以及和可運行的demo進行對比分析排查出問題所在祟蚀。
根源還在于不夠細心和理解的不透徹。
五割卖、資料
- OpenSL-ES 官方文檔
- NDK指南: OpenSL ES
- NDK指南demo:native-audio 是一個簡單的音頻錄制器/播放器
- 音視頻學習 (七) AudioTrack前酿、OpenSL ES 音頻渲染
- FFmpeg 開發(fā)(03):FFmpeg + OpenSL ES 實現(xiàn)音頻解碼播放
- android平臺OpenSL ES播放PCM數(shù)據(jù)
- Android通過OpenSL ES播放音頻套路詳解
六、收獲
- 了解了OpenSl ES的基本知識和播放音頻數(shù)據(jù)的流程
- 代碼實現(xiàn)OpenSL ES播放音頻流
- 和FFmpeg結合鹏溯,實現(xiàn)opensl播放解碼后的音頻數(shù)據(jù)
- 解決遇到的問題
感謝你的閱讀
學習實踐了視頻的解碼罢维、音頻的解碼和播放,下一篇我們通過OpenGL ES來實現(xiàn)解碼后視頻的渲染丙挽,歡迎關注公眾號“音視頻開發(fā)之旅”肺孵,一起學習成長。
歡迎交流