Android音視頻【十二】使用OpenSLES和AudioTrack進行播放PCM

人間觀察
年齡到了,有些事就妥協(xié)了,這個世界上沒有人可以隨心所欲,生活會逼著你選擇答案……最困難的是你什么都改變不了……

介紹

播放pcm的兩種方式

本節(jié)我們學(xué)習(xí)下如何播放pcm數(shù)據(jù),在Android中有兩種方法:一種是使用java層的AudioTrack方法,一種是使用底層的OpenSLES直接在jni層調(diào)用系統(tǒng)的OpenSLES的c方法實現(xiàn)。

使用場景

兩種使用場景不一樣:
AudioTrack 一般用于 比如本地播放一個pcm文件/流,又或者播放解碼后的音頻的pcm流,API較簡單。
OpenSLES 一般用于一些播放器中開發(fā)中,比如音頻/視頻播放器,聲音/音頻的播放采用的OpenSLES,一是播放器一般是c/c++實現(xiàn),便于直接在c層調(diào)用OpenSLES的API,二也是如果用AudioTrack進行播放,務(wù)必會帶來java和jni層的反射調(diào)用的開銷,API較復(fù)雜。

可以根據(jù)業(yè)務(wù)自行決定來進行選擇。

一.AudioTrack方式

AudioTrack的方式使用較簡單,直接在java層。

初始化

指定采樣率,采樣位數(shù),聲道數(shù)進行創(chuàng)建。

需要注意的是比如數(shù)據(jù)是解碼后的pcm數(shù)據(jù),如果每次的采樣率或者采樣位數(shù)或者聲道數(shù)和上次的不一樣,你需要銷毀重建AudioTrack,因為AudioTrack并沒有提供動態(tài)修改采樣率,采樣位數(shù),聲道數(shù)的方法,它只能在構(gòu)造方法中指定。

public void initAudioTrack() {
    int minBufferSize = AudioTrack.getMinBufferSize(44100,
            AudioFormat.CHANNEL_OUT_STEREO, AudioFormat.ENCODING_PCM_16BIT);
    audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,
            44100,
            AudioFormat.CHANNEL_OUT_STEREO,
            AudioFormat.ENCODING_PCM_16BIT,
            minBufferSize,
            AudioTrack.MODE_STREAM);
    audioTrack.play();
}

其中44100是采樣率,AudioFormat.CHANNEL_OUT_STEREO為雙聲道,還有CHANNEL_OUT_MONO單聲道。AudioFormat.ENCODING_PCM_16BIT為采樣位數(shù)16位,還有ENCODING_PCM_8BIT8位。minBufferSize是播放器緩沖的大小,也是根據(jù)采樣率和采樣位數(shù),聲道數(shù) 進行獲取,只有滿足最小的buffer才去操作底層進程播放。

最后一個參數(shù)mode??梢灾付ǖ闹涤?code>AudioTrack.MODE_STREAM和AudioTrack.MODE_STATIC。

MODE_STREAM 適用于大多數(shù)的場景,比如動態(tài)的處理audio buffer,或者播放很長的音頻文件,它是將audio buffers從java層傳遞到native層。音頻播放時音頻數(shù)據(jù)從Java流式傳輸?shù)絥ative層的創(chuàng)建模式。

MODE_STATIC 適用場景,比如播放很短的音頻,它是一次性將全部的音頻資源從java傳遞到native層。音頻數(shù)據(jù)在音頻開始播放前僅從Java傳輸?shù)絥ative層的創(chuàng)建模式。

寫入數(shù)據(jù)進行播放

public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes) {}

audioData 就是要播放的pcm數(shù)據(jù)
offsetInBytes audioData字節(jié)數(shù)組的的開始位置
sizeInBytes 要寫入audioData字節(jié)數(shù)組的大小
返回值 ,真實寫入的字節(jié)數(shù)

是的,就這么一個方法。注意此方法是同步方法,是個耗時方法,一般是開啟一個線程循環(huán)調(diào)用write方法進行寫入。
注意在調(diào)用write方法前需要調(diào)用 audioTrack.play()方法開始播放。

暫停銷毀等其他方法

mAudioTrack.pause(); // 暫停,注意下次恢復(fù)播放,需要重新調(diào)用play方法,然后循壞調(diào)用write寫入暫停后的數(shù)據(jù)即可
mAudioTrack.flush(); //  清空丟掉當(dāng)前排隊播放的音頻數(shù)據(jù)
mAudioTrack.stop(); // 停止播放音頻數(shù)據(jù)
mAudioTrack.release();// 銷毀播放器
mAudioTrack.setStereoVolume(volume, volume); 音量設(shè)置,范圍[0-1]
mAudioTrack.setVolume(float gain) 設(shè)置此軌道所有通道上的指定輸出增益值。

更多的API可以參考官網(wǎng)開發(fā)文檔。需要注意的是在有些手機上pause耗時,甚至耗時1s。

播放進度

因為是pcm裸數(shù)據(jù),無法像mediaplayer一樣提供了API。所以需要自己處理下??梢岳?code>getPlaybackHeadPosition方法。

getPlaybackHeadPosition()的意思是返回以幀為單位表示的播放頭位置
getPlaybackRate()的意思是返回以Hz為單位返回當(dāng)前播放采樣率。

所以當(dāng)前播放時間可以通過如下方式獲取

int currentFrame = mAudioTrack.getPlaybackHeadPosition();
LogUtil.dc(TAG, "currentFrame=" + currentFrame);
int rate = mAudioTrack.getPlaybackRate();
if (rate > 0) {
    float playTime = currentFrame * 1.0f / rate;
    currentPlayTimeMs = (long) (1000 * playTime);
    LogUtil.dc(TAG, "currentPlayTimeMs=" + currentPlayTimeMs);
}

二.OpenSLES方式

OpenSLES:(Open Sound Library for Embedded Systems).
OpenSLES是跨平臺是針對嵌入式系統(tǒng)精心優(yōu)化的硬件音頻加速API。使用OpenSLES進行音頻播放的好處是可以不依賴第三方。比如一些音頻或者視頻播放器中都是用OpenSLES進行播放解碼后的pcm的,這樣免去了和java層的交互。

使用OpenSLES

在Android中使用OpenSLES首先需要把Android 系統(tǒng)提供的so鏈接到外面自己的so。在CMakeLists.txt腳本中添加鏈接庫OpenSLES。庫的名字可以在 類似如下目錄中

/Users/guixiuzhong/Library/Android/sdk/ndk/21.1.6352462/platforms/android-19/arch-x86/usr/lib/libOpenSLES.so

需要去掉lib

target_link_libraries(
                OpenSLES
   // ...省略其它
        )

然后導(dǎo)入頭文件即可使用了OpenSLES提供的底層方法了。

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

創(chuàng)建OpenSLES

創(chuàng)建&使用的步驟大致分為:

  • 創(chuàng)建引擎 獲取SLEngineItf
  • 創(chuàng)建并設(shè)置混音器
  • 創(chuàng)建并設(shè)置播放器
  • 注冊播放器回調(diào)并寫入播放緩沖區(qū)隊列
  • 其它操作播放的方法,比如暫停,音量設(shè)置,聲道設(shè)置

創(chuàng)建引擎 獲取SLEngineItf

    SLresult result;
    result = slCreateEngine(&engineObject, 0, 0, 0, 0, 0);
    if (result != SL_RESULT_SUCCESS)
        return;
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    if (result != SL_RESULT_SUCCESS)
        return;
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    if (result != SL_RESULT_SUCCESS)
        return;
    if (engineEngine) {
        LOGD("get SLEngineItf success");
    } else {
        LOGE("get SLEngineItf failed");
    }
  • 創(chuàng)建引擎。使用slCreateEngine 第一個參數(shù)是要創(chuàng)建的引擎對象,是一個SLObjectItf類型。返回值是SLresult類型,如果成功則返回SL_RESULT_SUCCESS,其他參數(shù)都傳0即可。
  • 創(chuàng)建引擎成功后必須先調(diào)用Realize方法做初始化(*slObjectItf)->Realize(),實例化成功則返回SL_RESULT_SUCCESS
  • 引擎實例化之后從引擎對象獲取接口。
    SLresult (*GetInterface) (
        SLObjectItf self,  //實例化后的引擎對象
        const SLInterfaceID iid, //SL_IID_ENGINE
        void * pInterface  //輸出的接口對象指針
    );

一個SLObjectItf里面可能包含了多個Interface,獲取Interface通過GetInterface方法,而GetInterface方法的地2個參數(shù)SLInterfaceID參數(shù)來指定到的需要獲取Object里面的那個Interface。比如通過指定SL_IID_ENGINE的類型來獲取SLEngineItf。我們可以通過SLEngineItf去創(chuàng)建各種Object,例如播放器、錄音器、混音器的Object,然后在用這些Object去獲取各種Interface去實現(xiàn)各種功能。

創(chuàng)建混音器

如上所說,SLEngineItf可以創(chuàng)建混音器的Object。

  • 創(chuàng)建混音器。
const SLInterfaceID mids[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean mreq[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(
engineEngine, //引擎接口
 &outputMixObject,  //輸出的混音器
 1, mids, mreq);
if (result != SL_RESULT_SUCCESS) {
    LOGE("CreateOutputMix failed");
    return;
} else {
    LOGD("CreateOutputMix success");
}
  • 實例化混音器。拿到SLObjectItf 類型的實例化的混音器。
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
if (result != SL_RESULT_SUCCESS) {
    LOGE("mixer init failed");
} else {
    LOGD("mixer init success");
}
  • 實例化混音器后也可以通過混音器的GetInterface方法來調(diào)用接口等。

配置音頻信息

在創(chuàng)建播放器前需要創(chuàng)建音頻的配置信息(比如采樣率,聲道數(shù),每個采樣的位數(shù)等)

 //音頻格式
    SLDataFormat_PCM pcmFormat = {
            SL_DATAFORMAT_PCM, //播放pcm格式的數(shù)據(jù)
            2,   //聲道數(shù)
            static_cast<SLuint32>(getCurrentSampleRateForOpensles(sample_rate)),
            SL_PCMSAMPLEFORMAT_FIXED_16, //位數(shù) 16位
            SL_PCMSAMPLEFORMAT_FIXED_16, //和位數(shù)一致就行
            SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT, //立體聲(前左前右)
            //字節(jié)序,小端
            SL_BYTEORDER_LITTLEENDIAN
    };

創(chuàng)建播放器

  • 通過 引擎(*engineEngine)->CreateAudioPlayer 方法來創(chuàng)建播放器。
result = (*engineEngine)->CreateAudioPlayer(
engineEngine,  //引擎對象本身
&pcmPlayerObject, //輸出的播放器對象,同樣是SLObjectItf類型
&slDataSource, //數(shù)據(jù)的來源
&slDataSink,  //數(shù)據(jù)的去處,和SLDataSource是相對的
sizeof(ids) / sizeof(SLInterfaceID), //與下面的SLInterfaceID和SLboolean配合使用,用于標記SLInterfaceID數(shù)組和SLboolean的大小
ids,//這里需要傳入一個數(shù)組,指定創(chuàng)建的播放器會包含哪些Interface
req//這里也是一個數(shù)組,用來標記每個需要包含的Interface);
  • 獲取播放器接口
    (*pcmPlayerObject)->GetInterface(slPlayerItf, SL_IID_PLAY, &pcmPlayerPlay);得到播放器接口SLPlayItf pcmPlayerPlay。pcmPlayerPlay 之后就可以給播放器設(shè)置不同的狀態(tài)比如SL_PLAYSTATE_PAUSED進行播放暫停等操作,后文介紹。
    SLresult (*GetInterface) (
        SLObjectItf self, //實例化后的播放器對象
        const SLInterfaceID iid,  //SL_IID_PLAY
        void * pInterface //輸出的接口對象指針
    );
  • 獲取播放隊列接口
     result = (*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_BUFFERQUEUE, &pcmBufferQueue);
  • 給播放隊列注冊回調(diào)函數(shù)。

開始播放后會不斷的回調(diào)這個pcmBufferCallBack函數(shù)將音頻數(shù)據(jù)壓入隊列
(*pcmBufferQueue)->RegisterCallback(pcmBufferQueue, pcmBufferCallBack, this);

    // OpenSLES 會自動回調(diào)
void pcmBufferCallBack(SLAndroidSimpleBufferQueueItf bf, void *context) {
//    LOGD("pcmBufferCallBack ok");

    Audio *audio = (Audio *) context;
    if (audio != NULL) {
        PcmData *data = audio->dataQueue->getPcmData();
        if (NULL != data) {
            LOGD("Enqueue ok");
            (*audio->pcmBufferQueue)->Enqueue(audio->pcmBufferQueue,
                                              data->getData(),
                                              data->getSize());
        }
    }
}
  • 設(shè)置播放狀態(tài)為播放中
    //設(shè)置播放狀態(tài)
    (*pcmPlayerPlay)->SetPlayState(pcmPlayerPlay, SL_PLAYSTATE_PLAYING);

如果想要暫停播放參數(shù)直接設(shè)置為SL_PLAYSTATE_PAUSED,若暫停后繼續(xù)播放設(shè)置參數(shù)為SL_PLAYSTATE_PLAYING即可。若想要停止播放參數(shù)設(shè)置為SL_PLAYSTATE_STOPPED即可。

  • 開始播放
    需要手動調(diào)用一次 (*pcmBufferQueue)->Enqueue,也就是可以直接調(diào)用下 pcmBufferCallBack(pcmBufferQueue, this);

OpenSLES的音量控制

首先獲取播放器的用于控制音量的接口SLVolumeItf pcmVolumePlay

// 音量
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_VOLUME, &pcmVolumePlay);

然后動態(tài)設(shè)置

// 聲音0是最大聲音,-5000就聽不見了
// 音量 0 是最大,負值是越來越小。
float v = (1.0f - volume * 1.0f / 100.0f) * -5000;
LOGD("volume %f", v);
(*pcmVolumePlay)->SetVolumeLevel(pcmVolumePlay, (SLmillibel) v);

OpenSLES的聲道控制

首先也是獲取播放器的用于控制音量的接口SLMuteSoloItf pcmMutePlay

 // 獲取聲道操作接口
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_MUTESOLO, &pcmMutePlay);

然后動態(tài)設(shè)置

// 立體聲
(*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, false);
(*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, false);
// 左聲道
 (*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, true);
(*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, false);
// 右聲道
(*pcmMutePlay)->SetChannelMute(pcmMutePlay, 1, false);
(*pcmMutePlay)->SetChannelMute(pcmMutePlay, 0, true);

看起來控制還是蠻簡單的哈。先熟悉這么多,OpenSLES還是蠻強大的。

完整的源碼

https://github.com/ta893115871/PCMPlay

備注, OpenSLES的方式進行播放pcm,自己也是學(xué)習(xí)網(wǎng)上的一些文章和源碼,參考了下網(wǎng)上的代碼。僅供學(xué)習(xí)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容