## 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
## 前言
在講解音頻渲染之前,需要對音頻的基礎知識有所了解,所以該篇分為基礎概念和AudioTrack 以及 OpenSL ES Demo 實例講解,這樣有助于更好的理解 Android 中音頻渲染。
音頻的基礎概念涉及的知識點比較多,該篇文章的上半部分會詳細的介紹,后續文章基本上都會涉及音頻的開發,有了基礎對于后面的內容就更容易上手了。
## 音頻的基礎知識
### 聲音的物理性質
* 聲音是波
說到聲音我相信只要聽力正常的人都聽見過聲音,那么聲音是如何產生的呢?記得初中物理課本上的描述 - 聲音是由物體的振動而產生的。其實聲音是一種壓力波,當敲打某個物體或演奏某個樂器時,它們的振動都會引起空氣有節奏的振動,使周圍的空氣產生疏密變化,形成疏密相間的縱波,由此就產生了聲波,這種現象會一直延續到振動消失為止。
* 聲波的三要素
聲波的三要素是頻率、振幅、和波形,頻率代表音階的高低,振幅代表響度,波形代表音色。
* 聲音的傳播介質
聲音的傳播介質很廣,它可以通過空氣、液體和固體進行傳播;而且介質不同,傳播的速度也不同,比如聲音在空氣中的傳播速度為 340m/s , 在蒸餾水中的傳播速度為 1497 m/s , 而在鐵棒中的傳播速度則可以高達 5200 m/s ;不過,聲音在真空中時無法傳播的。
* 回聲
當我們在高山或者空曠地帶高聲大喊的時候,經常會聽到回聲,之所以會有回聲是因為聲音在傳播過程中遇到障礙物會反彈回來,再次被我們聽到。
但是,若兩種聲音傳到我們的耳朵里的時差小于 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.09 MB ,如果僅僅是將其存儲在光盤或者硬盤中,可能是可以接受的,但是若要在網絡中實時在線傳輸的話,那么這個數據量可能就太大了,所以必須對其進行壓縮編碼。壓縮編碼的基本指標之一就是壓縮比,壓縮比通常小于 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 主要應用于中低碼率的編碼 ( 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 平臺下的音頻渲染
音頻基礎概念上面講完了,下面我們實現 Android 下的音頻渲染,為實現音視頻播放器打下一個基礎,音視頻采集視頻錄制的時候在講解。
\[PCM 文件 - 鏈接:[pan.baidu.com/s/1ISS7bHMr…](https://pan.baidu.com/s/1ISS7bHMrFAentLB6o4kiBA)密碼:5z1n\](鏈接:[pan.baidu.com/s/1ISS7bHMr…](https://pan.baidu.com/s/1ISS7bHMrFAentLB6o4kiBA)密碼:5z1n)
### AudioTrack 的使用
由于 AudioTrack 是 Android SDK 層提供的最底層的 音頻播放 API,因此只允許輸入裸數據 PCM 。和 MediaPlayer 相比,對于一個壓縮的音頻文件(比如 MP3 、AAC 等文件),它只需要自行實現解碼操作和緩沖區控制。因為這里只涉及 AudioTrack 的音頻渲染端,編解碼我們后面在講解,所以本小節只介紹如何使用 AudioTrack 渲染音頻 PCM 裸數據。
1. 配置 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 ,需要按照一定的時間間隔不斷地寫入音頻數據,理論上它可以應用于任何音頻播放的場景。
2. Play
~~~
//當前播放實例是否初始化成功,如果處于初始化成功的狀態并且未播放的狀態,那么就調用 play
if (null != mAudioTrack && mAudioTrack.getState() != AudioTrack.STATE_UNINITIALIZED && mAudioTrack.getPlayState() != PLAYSTATE_PLAYING)
mAudioTrack.play();
復制代碼
~~~
3. 銷毀資源
~~~
public void release() {
Log.d(TAG, "==release===");
mStatus = Status.STATUS_NO_READY;
if (mAudioTrack != null) {
mAudioTrack.release();
mAudioTrack = null;
}
}
復制代碼
~~~
4. 具體實例請移步[AudioPlay 項目的 AudioTracker 部分](https://github.com/yangkun19921001/NDK_AV_SAMPLE/blob/master/audio_video/src/main/java/com/devyk/audiovideo/audio/AudioTracker.java),需要把項目中 raw 目錄下的 pcm 文件放入 sdcard 跟目錄中。
### OpenSL ES 的使用
[OpenSL ES 官方文檔](https://developer.android.google.cn/ndk/guides/audio/opensl-for-android)
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>
復制代碼
~~~
1. 創建引擎并獲取引擎接口
~~~
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;
}
}
復制代碼
~~~
2. 設置混音器
~~~
// 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;
}
復制代碼
~~~
3. 創建播放器
~~~
// 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;
}
復制代碼
~~~
4. 設置播放回調函數
~~~
// 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;
}
復制代碼
~~~
5. 設置播放器狀態
~~~
// TODO 第五大步:設置播放器狀態為播放狀態
result = (*mPlayer)->SetPlayState(mPlayer, SL_PLAYSTATE_PLAYING);
if (result != SL_RESULT_SUCCESS) {
LOGE("mPlayerObj SetPlayState failed: %d", result);
return false;
}
復制代碼
~~~
6. 手動激活回調函數
~~~
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;
}
復制代碼
~~~
7. 釋放資源
~~~
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](https://github.com/yangkun19921001/NDK_AV_SAMPLE/blob/master/audio_video/src/main/cpp/opensl/audio_play.cpp)部分。注意:需要把 raw 中的 pcm 文件放入 sdcard 根目錄下。
## 總結
該篇文章主要介紹了音頻的一些基礎知識和使用 AudioTrack 以及 OpenSL ES 來渲染裸流音頻數據。大家可以根據我的源代碼中在加深理解。
最后的頁面效果:

## 感謝
* [音視頻開發進階指南-展曉凱](https://github.com/zhanxiaokai)
- 前言
- JNI基礎知識
- C語言知識點總結
- ①基本語法
- ②數據類型
- 枚舉類型
- 自定義類型(類型定義)
- ③格式化輸入輸出
- printf函數
- scanf函數
- 編程規范
- ④變量和常量
- 局部變量和外部變量
- ⑤類型轉換
- ⑥運算符
- ⑦結構語句
- 1、分支結構(選擇語句)
- 2、循環結構
- 退出循環
- break語句
- continue語句
- goto語句
- ⑧函數
- 函數的定義和調用
- 參數
- 函數的返回值
- 遞歸函數
- 零起點學通C語言摘要
- 內部函數和外部函數
- 變量存儲類別
- ⑨數組
- 指針
- 結構體
- 聯合體(共用體)
- 預處理器
- 預處理器的工作原理
- 預處理指令
- 宏定義
- 簡單的宏
- 帶參數的宏
- 預定義宏
- 文件包含
- 條件編譯
- 內存中的數據
- C語言讀文件和寫文件
- JNI知識點總結
- 前情回顧
- JNI規范
- jni開發
- jni開發中常見的錯誤
- JNI實戰演練
- C++(CPP)在Android開發中的應用
- 掘金網友總結的音視頻開發知識
- 音視頻學習一、C 語言入門
- 1.程序結構
- 2. 基本語法
- 3. 數據類型
- 4. 變量
- 5. 常量
- 6. 存儲類型關鍵字
- 7. 運算符
- 8. 判斷
- 9. 循環
- 10. 函數
- 11. 作用域規則
- 12. 數組
- 13. 枚舉
- 14. 指針
- 15. 函數指針與回調函數
- 16. 字符串
- 17. 結構體
- 18. 共用體
- 19. typedef
- 20. 輸入 & 輸出
- 21.文件讀寫
- 22. 預處理器
- 23.頭文件
- 24. 強制類型轉換
- 25. 錯誤處理
- 26. 遞歸
- 27. 可變參數
- 28. 內存管理
- 29. 命令行參數
- 總結
- 音視頻學習二 、C++ 語言入門
- 1. 基本語法
- 2. C++ 關鍵字
- 3. 數據類型
- 4. 變量類型
- 5. 變量作用域
- 6. 常量
- 7. 修飾符類型
- 8. 存儲類
- 9. 運算符
- 10. 循環
- 11. 判斷
- 12. 函數
- 13. 數學運算
- 14. 數組
- 15. 字符串
- 16. 指針
- 17. 引用
- 18. 日期 & 時間
- 19. 輸入輸出
- 20. 數據結構
- 21. 類 & 對象
- 22. 繼承
- 23. 重載運算符和重載函數
- 24. 多態
- 25. 數據封裝
- 26. 接口(抽象類)
- 27. 文件和流
- 28. 異常處理
- 29. 動態內存
- 30. 命名空間
- 31. 預處理器
- 32. 多線程
- 總結
- 音視頻學習 (三) JNI 從入門到掌握
- 音視頻學習 (四) 交叉編譯動態庫、靜態庫的入門學習
- 音視頻學習 (五) Shell 腳本入門
- 音視頻學習 (六) 一鍵編譯 32/64 位 FFmpeg 4.2.2
- 音視頻學習 (七) 掌握音頻基礎知識并使用 AudioTrack、OpenSL ES 渲染 PCM 數據
- 音視頻學習 (八) 掌握視頻基礎知識并使用 OpenGL ES 2.0 渲染 YUV 數據
- 音視頻學習 (九) 從 0 ~ 1 開發一款 Android 端播放器(支持多協議網絡拉流/本地文件)
- 音視頻學習 (十) 基于 Nginx 搭建(rtmp、http)直播服務器
- 音視頻學習 (十一) Android 端實現 rtmp 推流
- 音視頻學習 (十二) 基于 FFmpeg + OpenSLES 實現音頻萬能播放器
- 音視頻學習 (十三) Android 中通過 FFmpeg 命令對音視頻編輯處理(已開源)