在講解音頻渲染以前,須要對音頻的基礎知識有所瞭解,因此該篇分爲基礎概念和AudioTrack 以及 OpenSL ES Demo 實例講解,這樣有助於更好的理解 Android 中音頻渲染。java
音頻的基礎概念涉及的知識點比較多,該篇文章的上半部分會詳細的介紹,後續文章基本上都會涉及音頻的開發,有了基礎對於後面的內容就更容易上手了。android
聲音是波c++
說到聲音我相信只要聽力正常的人都聽見過聲音,那麼聲音是如何產生的呢?記得初中物理課本上的描述 - 聲音是由物體的振動而產生的。其實聲音是一種壓力波,當敲打某個物體或演奏某個樂器時,它們的振動都會引發空氣有節奏的振動,使周圍的空氣產生疏密變化,造成疏密相間的縱波,由此就產生了聲波,這種現象會一直延續到振動消失爲止。git
聲波的三要素github
聲波的三要素是頻率、振幅、和波形,頻率表明音階的高低,振幅表明響度,波形表明音色。算法
聲音的傳播介質bash
聲音的傳播介質很廣,它能夠經過空氣、液體和固體進行傳播;並且介質不一樣,傳播的速度也不一樣,好比聲音在空氣中的傳播速度爲 340m/s , 在蒸餾水中的傳播速度爲 1497 m/s , 而在鐵棒中的傳播速度則能夠高達 5200 m/s ;不過,聲音在真空中時沒法傳播的。網絡
回聲架構
當咱們在高山或者空曠地帶高聲大喊的時候,常常會聽到回聲,之因此會有回聲是由於聲音在傳播過程當中遇到障礙物會反彈回來,再次被咱們聽到。ide
可是,若兩種聲音傳到咱們的耳朵裏的時差小於 80 毫秒,咱們就沒法區分開這兩種聲音了,其實在平常生活中,人耳也在收集回聲,只不過因爲嘈雜的外接環境以及回聲的分貝比較低,因此咱們的耳朵分辨不出這樣的聲音,或者說是大腦能接收到但分辨不出。
共鳴
天然界中有光能,水能,生活中有機械能,電能,其實聲音也能夠產生能量,例如兩個頻率相同的物體,敲打其中一個物體時另外一個物體也會振動發生。這種現象稱爲共鳴,共鳴證實了聲音傳播能夠帶動另外一個物體振動,也就是說,聲音的傳播過程也是一種能量的傳播過程。
上一小節咱們主要介紹了聲音的物理現象以及聲音中常見的概念,也會後續的講解統一了術語,本節主要介紹數字音頻概念。
爲了將模擬信號數字化,本節將分爲 3 個概念對數字音頻進行講解,分別是採樣、量化和編碼。首先要對模擬信號進行採樣,所謂採樣就是在時間軸上對信號進行數字化。根據奈奎斯特定理(也稱採樣定理),按比聲音最高頻率高 2 倍以上的頻率對聲音進行採樣,對於高質量的音頻信號,其頻率範圍在 20Hz ~ 20kHz ,因此採樣頻率通常爲 44.1kHz ,這樣就保證採樣聲音達到 20kHz 也能被數字化,從而使得通過數字化處理以後,人耳聽到的聲音質量不會被下降。而所謂的 44.1 kHz 就是表明 1 s 會採樣 44100 次。
那麼,具體的每一個採樣又該如何表示呢?這就涉及到將要講解的第二個概念: 量化。量化是指在幅度軸上對信號進行數字化,好比用 16 bit 的二進制信號來表示聲音的一個採樣,而 16 bit 所表示的範圍是 [-32768 , 32767] , 共有 65536 個可能取值,所以最終模擬的音頻信號在幅度上也分爲了 65536 層。
既然每個份量都是一個採樣,那麼這麼多的採樣該如何進行存儲呢?這就涉及將要講解的第三個概念: 編碼。所謂編碼,就是按照必定的格式記錄採樣和量化後的數字數據,好比順序存儲或壓縮存儲等等。
這裏涉及了不少中格式,一般所說的音頻的裸數據就是 PCM (Pulse Code Modulation) 數據。描述一段 PCM 數據通常須要如下幾個概念:量化格式(sampleFormat)、採樣率(sampleRate)、聲道數 (channel) 。以 CD 的音質爲例:量化格式爲 16 bit (2 byte),採樣率 44100 ,聲道數爲 2 ,這些信息就描述了 CD 的音質。而對於聲音的格式,還有一個概念用來描述它的大小,稱爲數據比特率,即 1s 時間內的比特數目,它用於衡量音頻數據單位時間內的容量大小。而對於 CD 音質的數據,比特率爲多少呢? 計算以下:
44100 * 16 * 2 = 1378.125 kbps
複製代碼
那麼在一分鐘裏,這類 CD 音質的數據須要佔據多大的存儲空間呢?計算以下:
1378.125 * 60 / 8 / 1024 = 10.09 MB
複製代碼
固然,若是 sampleFormat 更加精確 (好比用 4 個字節來描述一個採樣),或者 sampleRate 更加密集 (好比 48kHz 的採樣率), 那麼所佔的存儲空間就會更大,同時可以描述的聲音細節就會越精確。存儲的這段二進制數據即表示將模擬信號轉爲數字信號了,之後就能夠對這段二進制數據進行存儲,播放,複製,或者進行其它操做。
上面提到了 CD 音質的數據採樣格式,曾計算出每分鐘須要的存儲空間約爲 10.1MB ,若是僅僅是將其存儲在光盤或者硬盤中,多是能夠接受的,可是若要在網絡中實時在線傳輸的話,那麼這個數據量可能就太大了,因此必須對其進行壓縮編碼。壓縮編碼的基本指標之一就是壓縮比,壓縮比一般小於 1 。壓縮算法包括有損壓縮和無損壓縮。無所壓縮是指解壓後的數據不能徹底復原,會丟失一部分信息,壓縮比較小,丟失的信息就比較多,信號還原後的失真就會越大。根據不一樣的應用場景 (包括存儲設備、傳輸網絡環境、播放設備等),能夠選用不一樣的壓縮編碼算法,如 PCM 、WAV、AAC 、MP3 、Ogg 等。
WAV 編碼
WAV 編碼就是在 PCM 數據格式的前面加了 44 個字節,分別用來存儲 PCM 的採樣率、聲道數、數據格式等信息。
特色: 音質好,大量軟件支持。
場景: 多媒體開發的中間文件、保存音樂和音效素材。
MP3 編碼
MP3 具備不錯的壓縮比,使用 LAME 編碼 (MP3 編碼格式的一種實現)的中高碼率的 MP3 文件,聽感上很是接近源 WAV 文件,固然在不一樣的應用場景下,應該調整合適的參數以達到最好的效果。
特色: 音質在 128 Kbit/s 以上表現還不錯,壓縮比比較高,大量軟件和硬件都支持,兼容性好。
場景: 高比特率下對兼容性有要求的音樂欣賞。
AAC 編碼
AAC 是新一代的音頻有損壓縮技術,它經過一些附加的編碼技術(好比 PS 、SBR) 等,衍生出了 LC-AAC 、HE-AAC 、HE-AAC v2 三種主要的編碼格式。LC-AAC 是比較傳統的 AAC ,相對而言,其主要應用於中高碼率場景的編碼 (>=80Kbit/s) ; HE-AAC 至關於 AAC + SBR 主要應用於中低碼率的編碼 (<= 80Kbit/s); 而新推出的 HE-AAC v2 至關於 AAC + SBR + PS 主要用於低碼率場景的編碼 (<= 48Kbit/s) 。事實上大部分編碼器都設置爲 <= 48Kbit/s 自動啓用 PS 技術,而 > 48Kbit/s 則不加 PS ,至關於普通的 HE-AAC。
特色: 在小於 128Kbit/s 的碼率下表現優異,而且多用於視頻中的音頻編碼。
場景: 128 Kbit/s 如下的音頻編碼,多用於視頻中音頻軌的編碼。
Ogg 編碼
Ogg 是一種很是有潛力的編碼,在各類碼率下都有比較優秀的表現,尤爲是在中低碼率場景下。Ogg 除了音質好以外,仍是徹底免費的,這爲 Ogg 得到更多的支持打好了基礎,Ogg 有着很是出色的算法,能夠用更小的碼率達到更好的音質,128 Kbit/s 的 Ogg 比 192kbit/s 甚至更高碼率的 MP3 還要出色。可是目前由於尚未媒體服務軟件的支持,所以基於 Ogg 的數字廣播還沒法實現。Ogg 目前受支持的狀況還不夠好,不管是軟件上的仍是硬件上的支持,都沒法和 MP3 相提並論。
特色: 能夠用比 MP3 更小的碼率實現比 MP3 更好的音質,高中低碼率下均有良好的表現,兼容性不夠好,流媒體特性不支持。
場景: 語言聊天的音頻消息場景。
音頻基礎概念上面講完了,下面咱們實現 Android 下的音頻渲染,爲實現音視頻播放器打下一個基礎,音視頻採集視頻錄製的時候在講解。
[PCM 文件 - 連接:pan.baidu.com/s/1ISS7bHMr… 密碼:5z1n](連接:pan.baidu.com/s/1ISS7bHMr… 密碼:5z1n)
因爲 AudioTrack 是 Android SDK 層提供的最底層的 音頻播放 API,所以只容許輸入裸數據 PCM 。和 MediaPlayer 相比,對於一個壓縮的音頻文件(好比 MP3 、AAC 等文件),它只須要自行實現解碼操做和緩衝區控制。由於這裏只涉及 AudioTrack 的音頻渲染端,編解碼咱們後面在講解,因此本小節只介紹如何使用 AudioTrack 渲染音頻 PCM 裸數據。
配置 AudioTrack
public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, int mode) 複製代碼
streamType:Android 手機提供了多重音頻管理策略,當系統又多個進程須要播放音頻的時候,管理策略會決定最終的呈現效果,該參數的可選值將以常量的形式定義在類 AudioManager 中,主要包括如下內容:
/**電話鈴聲 */
public static final int STREAM_VOICE_CALL = AudioSystem.STREAM_VOICE_CALL;
/** 系統鈴聲 */
public static final int STREAM_SYSTEM = AudioSystem.STREAM_SYSTEM;
/** 鈴聲*/
public static final int STREAM_RING = AudioSystem.STREAM_RING;
/** 音樂聲 */
public static final int STREAM_MUSIC = AudioSystem.STREAM_MUSIC;
/** 警告聲 */
public static final int STREAM_ALARM = AudioSystem.STREAM_ALARM;
/** 通知聲 */
public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION;
複製代碼
sampleRateInHz:採樣率,即播放的音頻每秒鐘會有沒少次採樣,可選用的採樣頻率列表爲: 8000 , 16000 , 22050 , 24000 ,32000 , 44100 , 48000 等,你們能夠根據本身的應用場景進行合理的選擇。
channelConfig: 聲道數的配置,可選值以常量的形式配置在類 AudioFormat 中,經常使用的是 CHANNEL_IN_MONO (單聲道)、CHANNEL_IN_STEREO (雙聲道) ,由於如今大多數手機的麥克風都是僞立體聲採集,爲了性能考慮,建議使用單聲道進行採集。
audioFormat: 該參數是用來配置 "數據位寬" 的,即採樣格式,可選值以常量的形式定義在類 AudioFormat 中,分別爲 ENCODING_PCM_16BIT (兼容全部手機)、ENCODING_PCM_8BIT ,
bufferSizeInBytes: 配置內部的音頻緩衝區的大小, AudioTrack 類提供了一個幫助開發者肯定的 bufferSizeInBytes 的函數,其原型具體以下:
static public int getMinBufferSize(int sampleRateInHz, int channelConfig, int audioFormat) 複製代碼
在實際開發中,強烈建議由該函數計算出須要傳入的緩衝區大小,而不是手動計算。
mode: AudioTrack 提供了兩種播放模式,可選的值以常量的形式定義在類 AudioTrack 中,一個是 MODE_STATIC , 須要一次性將全部的數據都寫入播放緩衝區中,簡單高效,一般用於播放鈴聲、系統提醒的音頻片斷;另外一個是 MODE_STREAM ,須要按照必定的時間間隔不斷地寫入音頻數據,理論上它能夠應用於任何音頻播放的場景。
Play
//當前播放實例是否初始化成功,若是處於初始化成功的狀態而且未播放的狀態,那麼就調用 play
if (null != mAudioTrack && mAudioTrack.getState() != AudioTrack.STATE_UNINITIALIZED && mAudioTrack.getPlayState() != PLAYSTATE_PLAYING)
mAudioTrack.play();
複製代碼
銷燬資源
public void release() {
Log.d(TAG, "==release===");
mStatus = Status.STATUS_NO_READY;
if (mAudioTrack != null) {
mAudioTrack.release();
mAudioTrack = null;
}
}
複製代碼
具體實例請移步 AudioPlay 項目的 AudioTracker 部分,須要把項目中 raw 目錄下的 pcm 文件放入 sdcard 跟目錄中。
OpenSL ES 全稱(Open Sound Library for Embedded System) ,即嵌入式音頻加速標準。OpenSL ES 是無受權費、跨平臺、針對嵌入式系統精心優化的硬件音頻加速 API ,它能爲嵌入式移動多媒體設備上的本地應用程序開發者提供了標準化、高性能、低響應時間的音頻功能實現方法,同時還實現了軟/硬音頻性能的直接跨平臺部署,不只下降了執行難度,並且還促進了高級音頻市場的發展。
上圖描述了 OpenSL ES 的架構,在 Android 中,High Level Audio Libs 是音頻 Java 層 API 輸入輸出,屬於高級 API , 相對來講,OpenSL ES 則是比價低層級的 API, 屬於 C 語言 API 。在開發中,通常會直接使用高級 API , 除非遇到性能瓶頸,如語音實時聊天、3D Audio 、某些 Effects 等,開發者能夠直接經過 C/C++ 開發基於 OpenSL ES 音頻的應用。
在使用 OpenSL ES 的 API 以前,須要引入 OpenSL ES 的頭文件,代碼以下:
// 這是標準的OpenSL ES庫
#include <SLES/OpenSLES.h>
// 這裏是針對安卓的擴展,若是要垮平臺則須要注意
#include <SLES/OpenSLES_Android.h>
複製代碼
建立引擎並獲取引擎接口
void createEngine() {
// 音頻的播放,就涉及到了,OpenLSES
// TODO 第一大步:建立引擎並獲取引擎接口
// 1.1建立引擎對象:SLObjectItf engineObject
SLresult result = slCreateEngine(&engineObj, 0, NULL, 0, NULL, NULL);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 1.2 初始化引擎
result = (*engineObj) ->Realize(engineObj, SL_BOOLEAN_FALSE);
if (SL_BOOLEAN_FALSE != result) {
return;
}
// 1.3 獲取引擎接口 SLEngineItf engineInterface
result = (*engineObj) ->GetInterface(engineObj, SL_IID_ENGINE, &engine);
if (SL_RESULT_SUCCESS != result) {
return;
}
}
複製代碼
設置混音器
// TODO 第二大步 設置混音器
// 2.1 建立混音器:SLObjectItf outputMixObject
result = (*engine)->CreateOutputMix(engine, &outputMixObj, 0, 0, 0);
if (SL_RESULT_SUCCESS != result) {
return;
}
// 2.2 初始化 混音器
result = (*outputMixObj)->Realize(outputMixObj, SL_BOOLEAN_FALSE);
if (SL_BOOLEAN_FALSE != result) {
return;
}
複製代碼
建立播放器
// TODO 第三大步 建立播放器
// 3.1 配置輸入聲音信息
// 建立buffer緩衝類型的隊列 2個隊列
SLDataLocator_AndroidSimpleBufferQueue locBufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
// pcm數據格式
// SL_DATAFORMAT_PCM:數據格式爲pcm格式
// 2:雙聲道
// SL_SAMPLINGRATE_44_1:採樣率爲44100(44.1赫茲 應用最廣的,兼容性最好的)
// SL_PCMSAMPLEFORMAT_FIXED_16:採樣格式爲16bit (16位)(2個字節)
// SL_PCMSAMPLEFORMAT_FIXED_16:數據大小爲16bit (16位)(2個字節)
// SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT:左右聲道(雙聲道) (雙聲道 立體聲的效果)
// SL_BYTEORDER_LITTLEENDIAN:小端模式
SLDataFormat_PCM formatPcm = {SL_DATAFORMAT_PCM, (SLuint32) mChannels, mSampleRate,
(SLuint32) mSampleFormat, (SLuint32) mSampleFormat,
mChannels == 2 ? 0 : SL_SPEAKER_FRONT_CENTER,
SL_BYTEORDER_LITTLEENDIAN};
/* * Enable Fast Audio when possible: once we set the same rate to be the native, fast audio path * will be triggered */
if (mSampleRate) {
formatPcm.samplesPerSec = mSampleRate;
}
// 數據源 將上述配置信息放到這個數據源中
SLDataSource audioSrc = {&locBufq, &formatPcm};
// 3.2 配置音軌(輸出)
// 設置混音器
SLDataLocator_OutputMix locOutpuMix = {SL_DATALOCATOR_OUTPUTMIX, mAudioEngine->outputMixObj};
SLDataSink audioSink = {&locOutpuMix, nullptr};
/* * create audio player: * fast audio does not support when SL_IID_EFFECTSEND is required, skip it * for fast audio case */
// 須要的接口 操做隊列的接口
const SLInterfaceID ids[3] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME, SL_IID_EFFECTSEND};
const SLboolean req[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE};
// 3.3 建立播放器
result = (*mAudioEngine->engine)->CreateAudioPlayer(mAudioEngine->engine, &mPlayerObj,
&audioSrc, &audioSink,
mSampleRate ? 2 : 3, ids, req);
if (result != SL_RESULT_SUCCESS) {
LOGE("CreateAudioPlayer failed: %d", result);
return false;
}
// 3.4 初始化播放器:mPlayerObj
result = (*mPlayerObj)->Realize(mPlayerObj, SL_BOOLEAN_FALSE);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj Realize failed: %d", result);
return false;
}
// 3.5 獲取播放器接口:SLPlayItf mPlayerObj
result = (*mPlayerObj)->GetInterface(mPlayerObj, SL_IID_PLAY, &mPlayer);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj GetInterface failed: %d", result);
return false;
}
複製代碼
設置播放回調函數
// TODO 第四大步:設置播放回調函數
// 4.1 獲取播放器隊列接口:SLAndroidSimpleBufferQueueItf mBufferQueue
result = (*mPlayerObj)->GetInterface(mPlayerObj, SL_IID_BUFFERQUEUE, &mBufferQueue);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj GetInterface failed: %d", result);
return false;
}
// 4.2 設置回調 void playerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
result = (*mBufferQueue)->RegisterCallback(mBufferQueue, playerCallback, this);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj RegisterCallback failed: %d", result);
return false;
}
mEffectSend = nullptr;
if (mSampleRate == 0) {
result = (*mPlayerObj)->GetInterface(mPlayerObj, SL_IID_EFFECTSEND, &mEffectSend);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj GetInterface failed: %d", result);
return false;
}
}
result = (*mPlayerObj)->GetInterface(mPlayerObj, SL_IID_VOLUME, &mVolume);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj GetInterface failed: %d", result);
return false;
}
複製代碼
設置播放器狀態
// TODO 第五大步:設置播放器狀態爲播放狀態
result = (*mPlayer)->SetPlayState(mPlayer, SL_PLAYSTATE_PLAYING);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj SetPlayState failed: %d", result);
return false;
}
複製代碼
手動激活回調函數
void OpenSLAudioPlay::enqueueSample(void *data, size_t length) {
// 必須等待一幀音頻播放完畢後才能夠 Enqueue 第二幀音頻
pthread_mutex_lock(&mMutex);
if (mBufSize < length) {
mBufSize = length;
if (mBuffers[0]) {
delete[] mBuffers[0];
}
if (mBuffers[1]) {
delete[] mBuffers[1];
}
mBuffers[0] = new uint8_t[mBufSize];
mBuffers[1] = new uint8_t[mBufSize];
}
memcpy(mBuffers[mIndex], data, length);
// TODO 第六步:手動激活回調函數
(*mBufferQueue)->Enqueue(mBufferQueue, mBuffers[mIndex], length);
mIndex = 1 - mIndex;
}
複製代碼
釋放資源
extern "C"
JNIEXPORT void JNICALL Java_com_devyk_audioplay_AudioPlayActivity_nativeStopPcm(JNIEnv *env, jclass type) {
isPlaying = false;
if (slAudioPlayer) {
slAudioPlayer->release();
delete slAudioPlayer;
slAudioPlayer = nullptr;
}
if (pcmFile) {
fclose(pcmFile);
pcmFile = nullptr;
}
}
複製代碼
完整的代碼請參考倉庫中 OpenSL ES 部分。注意:須要把 raw 中的 pcm 文件放入 sdcard 根目錄下。
該篇文章主要介紹了音頻的一些基礎知識和使用 AudioTrack 以及 OpenSL ES 來渲染裸流音頻數據。你們能夠根據個人源代碼中在加深理解。
最後的頁面效果: