FFmpeg學習(三)音頻基礎


一:音頻入門

(一)聲音三要素

1.音調(音頻)

2.音量(振幅)

3.音色(諧波

粉色曲線是最接近自然界中的波形(基頻+多種不同頻率音頻合並:如黃色、藍色)
綠色曲線為基頻(主頻率),可以看到粉色曲線都是在主頻率上微調(走勢是基本一致的)
越接近正弦波,聲音一般越好聽,畸形或產生噪波

(二)模數轉換

模擬信號和數字信號之間可以相互轉換:

模擬信號一般通過PCM脈碼調制(Pulse Code Modulation)方法量化為數字信號即讓模擬信號的不同幅度(采樣大小)分別對應不同的二進制值,例如采用8位編碼可將模擬信號量化為2^8=256個量級,實用中常采取24位或30位編碼;

數字信號一般通過對載波進行移相(Phase Shift)的方法轉換為模擬信號。

補充:通常的采樣率(每秒采樣次數)為48000、32000、16000等等;采樣率越高,數據信號還原為模擬信號的還原度越高!!!

計算機、計算機局域網與城域網中均使用二進制數字信號,目前在計算機廣域網中實際傳送的則既有二進制數字信號,也有由數字信號轉換而得的模擬信號。但是更具應用發展前景的是數字信號。

(三)音頻原始數據

音視頻常用格式有以下兩種:
 PCM數據:純音頻數據; WAV(多媒體文件):既可以存儲PCM原始數據(多用),又可以存儲壓縮數據; 其實WAV就是在PCM原始數據之上,加上了一個頭,包含了基本信息(使得播放器使用正確的參數去播放PCM數據)

采樣率48000,位深度 16bit ,通道數2
知道這三個參數,那么基本我們就知道了
設備1秒內可以采集到多少音頻數據是:
48000 * 16 * 2 = 153600048000 * 16 * 2 / 8 = 192000 字節.

也就是我的設備在一秒內可以采集192000

這么大的碼流顯然無法在我們的網絡中傳輸!!!(不能帶寬全給音視頻傳輸吧),所以要進行音頻數據壓縮!!!

(四)音頻幀大小的計算(采樣率和時間間隔的區別)

采樣率是指在1秒中的采樣次數,而時間間隔是指每采取一幀音頻數據所要時間間隔!!!

假設音頻采樣率 = 8000,采樣通道 = 2,位深度 = 16,采樣間隔 = 20ms

首先我們計算一秒鍾總的數據量,采樣間隔采用20ms的話,說明每秒鍾需采集50次,這個計算大家應該都懂,那么總的數據量計算為

一秒鍾總的數據量 =8000 * 2*16/8 = 32000

所以每幀音頻數據大小 = 32000/50 = 640

每個通道樣本數 = 640/2 = 320

解析:#define MAX_AUDIO_FRAME_SIZE 192000 

是指雙通道下,采用48k采樣率,位深為16位,采樣時間為1s
  48000×2×16/8=192000字節

(五)PCM存儲格式

PCM存儲格式大體分為兩種Planner和Packed
我們以雙聲道為例,L表示左聲道,R表示右聲道,如下為兩種格式的存儲方式:

Planner

LLLLLLLL… RRRRRRRR…

Packed

LRLRLRLRLR…

FFMpeg中對音頻Format定義如下:

enum AVSampleFormat {
    AV_SAMPLE_FMT_NONE = -1,
    AV_SAMPLE_FMT_U8,          ///< unsigned 8 bits
    AV_SAMPLE_FMT_S16,         ///< signed 16 bits
    AV_SAMPLE_FMT_S32,         ///< signed 32 bits
    AV_SAMPLE_FMT_FLT,         ///< float
    AV_SAMPLE_FMT_DBL,         ///< double

    AV_SAMPLE_FMT_U8P,         ///< unsigned 8 bits, planar     AV_SAMPLE_FMT_S16P,        ///< signed 16 bits, planar     AV_SAMPLE_FMT_S32P,        ///< signed 32 bits, planar     AV_SAMPLE_FMT_FLTP,        ///< float, planar     AV_SAMPLE_FMT_DBLP,        ///< double, planar     AV_SAMPLE_FMT_S64,         ///< signed 64 bits
    AV_SAMPLE_FMT_S64P,        ///< signed 64 bits, planar 
    AV_SAMPLE_FMT_NB           ///< Number of sample formats. DO NOT USE if linking dynamically
};

我們這里以WASAPI為例,在windows中WASAPI捕獲的數據格式總是FLT即浮點Packed格式,而新版的FFMpeg中僅僅支持FLTP格式壓縮(應該是需要自行編譯FFMpeg庫並附加其他壓縮庫,如有錯請指正),目前網絡中的大部分博客均是老版FFMpeg所以還都在使用其他格式。因此我們需要對PCM數據進行重新采樣。

(六)WAV header

在查看音頻數據壓縮方式前,先了解WAV header的數據結構

WAV例子:

因為是4字節對齊,所以每個sample大小4字節;而之所以是4字節,取決於blockAlign字段=(幅度大小×通道數);

二:音頻處理流程

(一)直播客戶端的處理流程

我們講解的音視頻都是基於娛樂直播進行講解的,所以我們必須對整個流程非常清楚。

如下圖,是一個基本的直播客戶端的處理流程圖:

直播客戶端分為幾個模塊,我們需要對每個模塊做的事情非常清楚。總的來說我們直播客戶端分為兩個端:共享端觀看端

共享端包含:音視頻采集,音視頻編碼兩個模塊
觀看端包含:音視頻解碼,音視頻渲染兩個模塊。

1.音視頻采集:

音頻采集一般我們只需要簡單調用一下api就能實現,每個操作系統都提供了相對應的api, 只是對於不同的平台各不相同。
如Android端有audioRecord, mediaRecord等,我們什么時候使用audioRecord,什么時候使用MediaRecord,這個我們需要非常清楚它的應用場景。
同樣在IOS 端有蘋果提供的AVFoundation框架提供了很多音視頻采集的方法,其中有很常用的底層方法AudioUnit。

2.音視頻編碼:

我們采集到音視頻之后並不能直接傳輸,因為這個數據太大了,超出了我們網路的設備的負載。
如果我們直接傳輸這么大量的數據,很容易出現各種問題,因此我們必須對這些數據進行壓縮后再進行傳輸,這個壓縮的過程就是對音視頻的編碼處理。
編碼后的數據就是非常小的數據了。
對於編碼這塊的知識也非常多,我們后續也會詳細講解。
編碼分為有損編碼和無損編碼。我們什么時候用有損編碼,什么時候使用無損編碼? 有損編碼去掉的是哪些數據?這些原理我們都需要很清楚明白。

3.音視頻傳輸:

編碼時候,我們就需要把數據傳輸到對端,這個傳輸的過程也是非常復雜的,需要對網絡知識有個較好的理解。后續我也講到這些。

4.音視頻解碼:

對端收到傳輸的數據后,需要對編碼的數據進行解碼,把壓縮的數據還原后才能進行播放渲染解碼這塊是跟編碼相對應的,用什么方式編碼,就需要用對應的方式解碼。

5.音視頻渲染:

解碼獲取得到原始數據之后,音頻交給揚聲器播放(實際是交給驅動,驅動交給驅動硬件模塊進行處理),視頻交給渲染器進行渲染。

(二)音頻數據的流轉(格式的轉換)

我們需要知道我們采集數據后,采集的是什么數據,是PCM數據(模擬數據轉數字信號,數字信號就是PCM)。然后經過編碼之后,編碼出的是什么格式的數據(aac/mp3)。這都是我們需要理解的。

對於錄制的視頻,我們最后將音視頻保存入容器中;對於直播來說,我們並不需要最后一步,不需要將他存入容器中!!!

如下圖是音頻數據流的流轉過程:

三:音頻采集

(一)各平台音頻采集方式

1.Android端音頻采集https://blog.csdn.net/u010029439/article/details/85056767

android平台上的音頻采集一般就兩種方式:

(1)使用MediaRecorder進行音頻采集。

MediaRecorder 是基於 AudioRecorder 的 API(最終還是會創建AudioRecord用來與AudioFlinger進行交互) ,它可以直接將采集到的音頻數據轉化為執行的編碼格式,並保存
這種方案相較於調用系統內置的用用程序,便於開發者在UI界面上布局,而且系統封裝的很好,便於使用,唯一的缺點是使用它錄下來的音頻是經過編碼的,沒有辦法的得到原始的音頻。
同時MediaRecorder即可用於音頻的捕獲也可以用於視頻的捕獲相當的強大。實際開發中沒有特殊需求的話,用的是比較多的!

(2)使用AudioRecord進行音頻采集。

AudioRecord 是一個比較偏底層的API,它可以獲取到一幀幀PCM數據,之后可以對這些數據進行處理。
AudioRecord這種方式采集最為靈活,使開發者最大限度的處理采集的音頻,同時它捕獲到的音頻是原始音頻PCM格式的!
像做變聲處理的需要就必須要用它收集音頻。

在實際開發中,它也是最常用來采集音頻的手段。如直播技術采用的就是AudioRecorder采集音頻數據。

2.Ios端音頻采集https://blog.51cto.com/u_13505171/2057075

3.Windows音頻采集:https://blog.csdn.net/machh/article/details/83546258

(二)FFMpeg采集音頻方式(集成上面所有平台)

(1)通過命令方式(不同平台可能不同):https://www.cnblogs.com/ssyfj/p/14576359.html

先查看可用設備:

 

其中card中:PCH與NVidia是聲卡類型,可以通過查看cat /proc/asound/cards文件查看所有在主機中注冊的聲卡列表:

其中device設備中:

HDMI:是高清多媒體接口(High Definition Multimedia Interface,HDMI)是一種全數字化視頻和聲音發送接口,可以發送未壓縮的音頻及視頻信號。
alc662是聲卡:聲卡 (Sound Card)也叫音頻卡(港台稱之為聲效卡),是計算機多媒體系統中最基本的組成部分,是實現聲波/數字信號相互轉換的一種硬件。聲卡的基本功能是把來自話筒、磁帶、光盤的原始聲音信號加以轉換,輸出到耳機、揚聲器、擴音機、錄音機等聲響設備,或通過音樂設備數字接口(MIDI)發出合成樂器的聲音

所以:想要錄取聲音,我們必須選取card:0,其他的device與subdevice可以任意

選取對應設備獲取輸入:

ffmpeg -f alsa -i hw:0 alsaout.wav

(2)通過API方式:如下(三)

(三)FFmpeg編程采集音頻

1.打開輸入設備 

#include <libavutil/log.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>

int main(int argc,char* argv)
{
    char* devicename = "hw:0";
    char errors[1024];
    int ret;

    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    //注冊所有的設備,包括我們需要的音頻設備
    avdevice_register_all();

    //獲取輸入(采集)格式
    AVInputFormat *iformat = av_find_input_format("alsa");

    //打開輸入設備
    AVFormatContext* fmt_ctx=NULL;
    AVDictionary* options=NULL;
    ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options);
    if(ret<0){
        av_strerror(ret,errors,1024);
        av_log(NULL,AV_LOG_ERROR,"Failed to open audio device,[%d]%s\n",ret,errors);
    }
    av_log(NULL,AV_LOG_INFO,"Success to open audio device\n");

    return 0;
}
View Code
gcc -o od 01OpenDevice.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice

2.從音頻設備中讀取音頻數據 

#include <libavutil/log.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>

int main(int argc,char* argv)
{
    char* devicename = "hw:0";
    char errors[1024];
    int ret,count=0;

    AVFormatContext* fmt_ctx=NULL;
    AVDictionary* options=NULL;
    AVInputFormat *iformat=NULL;
    AVPacket packet;    //包結構

    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    //注冊所有的設備,包括我們需要的音頻設備
    avdevice_register_all();

    //獲取輸入(采集)格式
    iformat = av_find_input_format("alsa");

    //打開輸入設備
    ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options);
    if(ret<0){
        av_strerror(ret,errors,1024);
        av_log(NULL,AV_LOG_ERROR,"Failed to open audio device,[%d]%s\n",ret,errors);
    }
    
    av_log(NULL,AV_LOG_INFO,"Success to open audio device\n");
    //開始從設備中讀取數據
    while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){
        av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count);

        //釋放空間
        av_packet_unref(&packet);
    }

    //關閉設備、釋放上下文空間
    avformat_close_input(&fmt_ctx);
    return 0;
}
View Code
gcc -o gap 02GetAudioPacket.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice

包的大小同碼率/8=字節數

3.錄制成為音頻文件 

創建文件--->將音頻數據寫入到文件中--->關閉文件
#include <stdio.h>
#include <libavutil/log.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>

int main(int argc,char* argv)
{
    char* devicename = "hw:0";
    char errors[1024];
    int ret,count=0,len;
    FILE* fp = NULL;

    AVFormatContext* fmt_ctx=NULL;
    AVDictionary* options=NULL;
    AVInputFormat *iformat=NULL;
    AVPacket packet;    //包結構

    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    //注冊所有的設備,包括我們需要的音頻設備
    avdevice_register_all();

    //獲取輸入(采集)格式
    iformat = av_find_input_format("alsa");

    //打開輸入設備
    ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options);
    if(ret<0){
        av_strerror(ret,errors,1024);
        av_log(NULL,AV_LOG_ERROR,"Failed to open audio device,[%d]%s\n",ret,errors);
    }
    
    av_log(NULL,AV_LOG_INFO,"Success to open audio device\n");
    //打開文件
    fp = fopen("./audio.pcm","wb");
    if(fp==NULL){
        av_log(NULL,AV_LOG_ERROR,"Failed to open out file,[%d]%s\n",ret,errors);
        goto fail;
    }

    //開始從設備中讀取數據
    while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){
        av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count);
        len = fwrite(packet.data,packet.size,1,fp);
        fflush(fp);
        if(len!=packet.size){
            av_log(NULL,AV_LOG_WARNING,"Warning,Packet size:%d not equal writen size:%d\n",len,packet.size);
        }else{
            av_log(NULL,AV_LOG_INFO,"Success write Packet to file");
        }
        //釋放空間
        av_packet_unref(&packet);
    }

fail:
    if(fp)
        fclose(fp);
    //關閉設備、釋放上下文空間
    avformat_close_input(&fmt_ctx);
    return 0;
}
View Code
gcc -o wad 03WriteAudioData.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice

注意:播放時需要指定格式,通過查看配置文件獲取采樣率等信息

sudo gedit /etc/pulse/daemon.conf

ffplay -ar 44100 -ac 2 -f s16le audio.pcm 

四:音頻壓縮

(一)音頻有損壓縮技術(消除冗余信息)

注意:頻域遮蔽效應和時域遮蔽效應的縱軸都是聲音強度;
頻域遮蔽的橫軸是頻率,是指在頻率相近的聲音,在一定的范圍內(遮蔽門檻下的聲音),聲音強度大的會遮蔽聲音強度小的聲音。
時域遮蔽的橫軸是時間,是指在時間相近的聲音,聲音強度大的會遮蔽聲音強度小的聲音,而且在發聲前的部分較弱的聲音(前遮蔽)和發聲后的部分較弱的聲音都會被屏蔽掉(后遮蔽)。

(二)音頻無損壓縮技術

哈夫曼編碼
算術編碼
香農編碼

(三)音頻編碼過程

原始數據--->傳入給兩個模塊進行處理-------------------------------->將兩種數據(真正需要編碼的數據)匯總,進行量化,編碼-------->形成比特流
           1.時域轉頻域變換(將一段長時間的數據,轉換為多種頻段的數據,從而獲取我們需要的頻段數據)
           2.心理聲學模型(前面的有損壓縮過程)

(四)常見的音頻編碼器

 

其中最常見的是OPUS和AAC:延遲小,壓縮率高

實時互動系統可以用opus(在線教育、會議),其中WebRTC默認使用OPUS
泛娛樂化直播一般使用AAC(最廣泛),opus一般不支持,推廣上有些困難
兩個系統融合,需要將opus與AAC互轉
speex:回音消除,降噪模塊等可實現。
G.711:有些會與固話相聯系,固話用的就是G.711,或者G.722 (聲音損失嚴重,但是可以在窄寬帶下傳輸)

音頻編碼器性能質量對比:

橫軸:比特率越大(質量越好),但是傳輸速度可能較低;所以對於實時性要求高的,如OPus,在窄帶時,會降低質量,從而提高傳輸速度
縱軸:帶寬質量(窄帶、寬帶、全帶),硬件設施有關

音頻編碼碼率對比:(結合上面性能圖)

縱軸是延遲(0~200ms),橫軸是碼率。延遲越低,碼率越小:
Opus延遲較低,所以在20ms內,碼率從0~20kb
/s 而在碼率較小時(0~20kb/s),AAC延遲則較大,在200ms附近,所以AAC不適用於實時性直播,適合於有一定延遲的直播。如果使用AAC於實時通信,那么可以選擇AAC-LD低延遲類型

五:AAC編碼

(一)AAC編碼器介紹

MP3相對來說存儲的壓縮比較低,壓縮后的文件還是比較大。而AAC壓縮率高,壓縮后文件較小,並且保真性能好,還原數據后,與原始數據相似性高。
AAC常用規格中:AAC HE V1使用較少,因為被V2取代。通過下面AAC規格可以了解:
  AAC LC 最基礎
  AAC HE V1 = AAC LC + SBR
  AAC HE V2 = AAC HE V1 + PS

AAC LC:碼流越大(AAC LC,128k),存儲信息量越大,音質越好,保真性高。碼流越小,壓縮比越高,去除的冗余信息多,會對重要的數據造成一定的損失。

AAC HE:按頻譜分別保存;低頻(基頻)保存主要成分,高頻(諧頻)和音色有很大關系,將高頻單獨放大去保證音質。實現進一步壓縮。

AAC HE V2:參數化,保存差異,進一步減少碼流大小。

對於AAC格式:ADTS相對於ADIF而言,雖然每一幀前面都有header信息,但是卻可以實現隨時拖動播放,不必每次從頭播放

補充:音頻一幀數據計算---假設音頻采樣率 = 8000,采樣通道 = 2,位深度 = 8,采樣間隔 = 20ms

首先我們計算一秒鍾總的數據量,采樣間隔采用20ms的話,說明每秒鍾需采集50次(1s=1000ms),那么總的數據量計算為

一秒鍾總的數據量 =8000 * 2*8/8 = 16000(Byte)

所以每幀音頻數據大小 = 16000/50 =320(Byte)

每個通道樣本數 = 320/2 = 160(Byte)

(二)ADTS格式

ADTS頭,適用性廣,更加適用於流的傳輸,尤其對於直播系統

每一幀的ADTS的頭文件都包含了音頻的采樣率,聲道,幀長度等信息,這樣解碼器才能解析讀取。

一般情況下ADTS的頭信息都是7個字節,分為2部分:

adts_fixed_header();

adts_variable_header();

1.adts_fixed_header();

syncword :總是0xFFF, 代表一個ADTS幀的開始, 用於同步。解碼器可通過0xFFF確定每個ADTS的開始位置。因為它的存在,解碼可以在這個流中任何位置開始, 即可以在任意幀解碼。
ID:MPEG Version: 0 for MPEG-41 for MPEG-2
Layer:always: '00'
protection_absent:Warning, set to 1 if there is no CRC and 0 if there is CRC  #0表示需要CRC校驗,1不需要
profile:表示使用哪個級別的AAC,如01 Low Complexity(LC) -- AAC LC; profile的值等於 Audio Object Type的值減1。 profile=(audio_object_type - 1)

sampling_frequency_index:采樣率的下標

channel_configuration:聲道數,比如2表示立體聲雙聲道

2.adts_variable_header();

aac_frame_length:一個ADTS幀的長度包括ADTS頭和AAC原始流。frame length, this value must include 7 or 9 bytes of header length:
            aac_frame_length = (protection_absent == 1 ? 7 : 9) + size(AACFrame)

            protection_absent=0時, header length=9bytes
            protection_absent=1時, header length=7bytes
adts_buffer_fullness:0x7FF 說明是碼率可變的碼流。
number_of_raw_data_blocks_in_frame:表示ADTS幀中有number_of_raw_data_blocks_in_frame + 1個AAC原始幀。
                      所以說number_of_raw_data_blocks_in_frame == 0 表示說ADTS幀中有一個AAC數據塊。
                      (一個AAC原始幀包含一段時間內1024個采樣及相關數據)

3.代碼實現頭部的添加:https://www.cnblogs.com/ssyfj/p/14579909.html

void adts_header(char *szAdtsHeader, int dataLen){

    int audio_object_type = 2;             //通過av_dump_format顯示音頻信息或者ffplay獲取多媒體文件的音頻流編碼acc(LC),對應表格中Object Type ID -- 2
    int sampling_frequency_index = 4;      //音頻信息中采樣率為44100 Hz 對應采樣率索引0x4
    int channel_config = 2;                   //音頻信息中音頻通道為雙通道2

    int adtsLen = dataLen + 7;             //采用頭長度為7字節,所以protection_absent=1   =0時為9字節,表示含有CRC校驗碼

    szAdtsHeader[0] = 0xff;         //syncword :總是0xFFF, 代表一個ADTS幀的開始, 用於同步. 高8bits

    szAdtsHeader[1] = 0xf0;         //syncword:0xfff                          低4bits
    szAdtsHeader[1] |= (0 << 3);    //MPEG Version:0 : MPEG-4(mp4a),1 : MPEG-2  1bit
    szAdtsHeader[1] |= (0 << 1);    //Layer:0                                   2bits 
    szAdtsHeader[1] |= 1;           //protection absent:1  沒有CRC校驗            1bit

    szAdtsHeader[2] = (audio_object_type - 1)<<6;            //profile=(audio_object_type - 1) 表示使用哪個級別的AAC  2bits
    szAdtsHeader[2] |= (sampling_frequency_index & 0x0f)<<2; //sampling frequency index:sampling_frequency_index  4bits 
    szAdtsHeader[2] |= (0 << 1);                             //private bit:0                                      1bit
    szAdtsHeader[2] |= (channel_config & 0x04)>>2;           //channel configuration:channel_config               高1bit

    szAdtsHeader[3] = (channel_config & 0x03)<<6;     //channel configuration:channel_config      低2bits
    szAdtsHeader[3] |= (0 << 5);                      //original:0                               1bit
    szAdtsHeader[3] |= (0 << 4);                      //home:0                                   1bit ----------------固定頭完結,開始可變頭
    szAdtsHeader[3] |= (0 << 3);                      //copyright id bit:0                       1bit  
    szAdtsHeader[3] |= (0 << 2);                      //copyright id start:0                     1bit
    szAdtsHeader[3] |= ((adtsLen & 0x1800) >> 11);    //frame length:value                       高2bits  000|1 1000|0000 0000

    szAdtsHeader[4] = (uint8_t)((adtsLen & 0x7f8) >> 3);     //frame length:value    中間8bits             0000  0111 1111 1000
    
    szAdtsHeader[5] = (uint8_t)((adtsLen & 0x7) << 5);       //frame length:value    低 3bits              0000  0000 0000 0111
    //number_of_raw_data_blocks_in_frame:表示ADTS幀中有number_of_raw_data_blocks_in_frame + 1個AAC原始幀。所以說number_of_raw_data_blocks_in_frame == 0 表示說ADTS幀中有一個AAC數據塊。(一個AAC原始幀包含一段時間內1024個采樣及相關數據)
    szAdtsHeader[5] |= 0x1f;                                 //buffer fullness:0x7ff 高5bits   0x7FF 說明是碼率可變的碼流 ---> 111 1111 1111 00----> 1 1111 1111 1100--->0x1f與0xfc

    szAdtsHeader[6] = 0xfc;                                        
}

4.AAC首部分析:https://www.p23.nl/projects/aac-header/

5.命令行提取AAC文件

 

部分參數可以通過ffplay播放來獲取:

ffmpeg -i gfxm.mp4 -vn -c:a aac -ar 44100 -channels 2 -profile:a 2 1.aac

ffmpeg -i gfxm.mp4 -vn -c:a libfdk_aac -ar 44100 -channel 2 -profile:a aac_he_v2 1.aac

補充:通過profile參數,修改編碼級別,2表示修改為AAC SSR級別

ffplay 1.aac 

編程實現編碼器:如七所示

六:音頻重采樣

(一)什么是音頻重采樣

1.為什么需要重采樣?

2.如何知道是否需要進行重采樣?

3.重采樣的步驟

4.重采樣相關API 

(二)編程實現音頻重采樣

1.補充:

音頻通道布局

FFMpeg筆記(三) 音頻處理基本概念及音頻重采樣

2.代碼實現

#include <stdio.h>
#include <libavutil/log.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>

void rec_audio(){
    char* devicename = "hw:0";
    char errors[1024];
    int ret,count=0,len;
    FILE* fp = NULL;

    AVFormatContext* fmt_ctx=NULL;
    AVDictionary* options=NULL;
    AVInputFormat *iformat=NULL;
    AVPacket packet;    //包結構

    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    //注冊所有的設備,包括我們需要的音頻設備
    avdevice_register_all();

    //獲取輸入(采集)格式
    iformat = av_find_input_format("alsa");

    //打開輸入設備
    ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options);
    if(ret<0){
        av_strerror(ret,errors,1024);
        av_log(NULL,AV_LOG_ERROR,"Failed to open audio device,[%d]%s\n",ret,errors);
    }
    
    av_log(NULL,AV_LOG_INFO,"Success to open audio device\n");
    //打開文件
    fp = fopen("./audio_2.pcm","wb");
    if(fp==NULL){
        av_log(NULL,AV_LOG_ERROR,"Failed to open out file\n");
        goto fail;
    }

    //----------創建重采樣的上下文
    SwrContext* swr_cxt = NULL;
    swr_cxt = swr_alloc_set_opts(NULL,                    //設置已經創建好的上下文,如果沒有,則為NULL
                                AV_CH_LAYOUT_STEREO,    //設置輸出目標的通道布局(雙聲道,立體聲,...,方位增寬)
                                AV_SAMPLE_FMT_FLT,        //設置輸出目標的采樣格式,設置為32位浮點型
                                44100,                    //設置輸出目標的采樣率
                                AV_CH_LAYOUT_STEREO,    //輸入數據的通道布局,是雙聲道
                                AV_SAMPLE_FMT_S16,        //輸入數據的采樣格式為s16le
                                44100,                    //輸入的采樣率
                                0,                        //日志級別
                                NULL);                    //日志上下文

    if(!swr_cxt){
        av_log(NULL,AV_LOG_ERROR,"Failed to set swr context\n");
        goto fail;
    }

    //----------初始化上下文
    if(swr_init(swr_cxt)<0){
        av_log(NULL,AV_LOG_ERROR,"Failed to initial swr context\n");
        goto fail;
    }

    //----------構造輸入、輸出空間
    uint8_t** src_data = NULL;
    uint8_t** dst_data = NULL;
    int src_linesize = 0,dst_linesize=0;
    av_samples_alloc_array_and_samples(&src_data,            //前面兩個傳入地址,用作輸出;其他作為輸入數據
                                        &src_linesize,
                                        2,                    //雙通道
                                        512,                //單通道采樣個數 每個packet大小2048,2048/2(和下面采樣格式有關)=1024個采樣個數,1024/2(這里是雙通道)=512(是單通道)
                                        AV_SAMPLE_FMT_S16,    //采樣格式
                                        0);                    //對齊


    av_samples_alloc_array_and_samples(&dst_data,            //前面兩個傳入地址,用作輸出;其他作為輸入數據
                                        &dst_linesize,
                                        2,                    //雙通道
                                        512,                //輸出的單通道采樣個數,和上面一致即可,只是對每個采樣進行了數據轉換,對內部數據重采樣,沒有修改采樣個數
                                        AV_SAMPLE_FMT_FLT,    //采樣格式
                                        0);                    //對齊

    //開始從設備中讀取數據
    while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){
        av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count);

        //----------先對數據進行重采樣,然后寫入文件中
        memcpy((void*)src_data[0],(void*)packet.data,packet.size);

        swr_convert(swr_cxt,                                //上下文
                    dst_data,                                //輸出數組(雙指針)
                    512,                                    //每個通道的采樣數
                    (const uint8_t**)src_data,                //輸入數據,來自與packet.data,要改造格式
                    512);                                    //輸入的通道采樣數

        len = fwrite(dst_data[0],1,dst_linesize,fp);
        fflush(fp);

        if(len!=dst_linesize){
            av_log(NULL,AV_LOG_WARNING,"Warning,Packet size:%d not equal writen size:%d\n",len,packet.size);
        }else{
            av_log(NULL,AV_LOG_INFO,"Success write Packet to file\n");
        }
        //釋放空間
        av_packet_unref(&packet);
    }

fail:

    if(fp)
        fclose(fp);
    //----------釋放空間
    if(src_data){
        av_freep(&src_data[0]);
    }
    av_freep(&src_data);

    if(dst_data){
        av_freep(&dst_data[0]);
    }
    av_freep(&dst_data);

    swr_free(&swr_cxt);
    
    //關閉設備、釋放上下文空間
    avformat_close_input(&fmt_ctx);
    return ;
}

int main(int argc,char* argv)
{
    rec_audio();
    return 0;
}
View Code
gcc -o ra 04ResampleAudio.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice -lswresample

 

七:創建AAC編碼器

見六:Audio:pcm_s16le,說明數據為原始pcm數據,沒有進行編碼處理!!!

(一)FFmpeg編碼過程

1.創建並打開編碼器

2.獲取輸入輸出數據

avcodec_send_frame:將幀(frame)發送給編碼器
avcodedc_receive_packet:通過receive_packet獲取編碼后的數據(packet)

一般來說AVPacket中存放的是編碼后的數據,而AVFrame中存放的未編碼的數據!!

疑問:為什么前面實現代碼,是從設備中獲取packet,而不是上面所說的frame??

與前面avformat_open_input有關,是將打開的設備當作多媒體文件進行處理。而多媒體文件肯定是編碼后的數據。所以會認為從設備中讀取的數據應該是編碼后的數據,使用pakcet獲取。而實際上數據類型應該是frame。
所以從設備中讀取的數據並沒有走編碼過程,實際數據還是pcm數據(見六圖中Audio:pcm_s16le)

(二)FFmpeg代碼實現(重采樣+編碼為AAC

#include <stdio.h>
#include <libavutil/log.h>
#include <libavcodec/avcodec.h>
#include <libavdevice/avdevice.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>

SwrContext* initSwrCxt(uint8_t*** src_data,int* src_linesize,uint8_t*** dst_data,int* dst_linesize){
    //創建重采樣的上下文
    SwrContext* swr_cxt = NULL;
    swr_cxt = swr_alloc_set_opts(NULL,                    //設置已經創建好的上下文,如果沒有,則為NULL
                                AV_CH_LAYOUT_STEREO,    //設置輸出目標的通道布局(雙聲道,立體聲,...,方位增寬)
                                AV_SAMPLE_FMT_S16,        //設置輸出目標的采樣格式,設置為32位浮點型
                                44100,                    //設置輸出目標的采樣率
                                AV_CH_LAYOUT_STEREO,    //輸入數據的通道布局,是雙聲道
                                AV_SAMPLE_FMT_S16,        //輸入數據的采樣格式為s16le
                                44100,                    //輸入的采樣率
                                0,                        //日志級別
                                NULL);                    //日志上下文

    if(!swr_cxt){
        av_log(NULL,AV_LOG_ERROR,"Failed to set swr context\n");
        return NULL;
    }

    //初始化上下文
    if(swr_init(swr_cxt)<0){
        av_log(NULL,AV_LOG_ERROR,"Failed to initial swr context\n");
        return NULL;
    }

    //構造輸入、輸出空間
    av_samples_alloc_array_and_samples(src_data,            //前面兩個傳入地址,用作輸出;其他作為輸入數據
                                        src_linesize,
                                        2,                    //雙通道
                                        512,                //單通道采樣個數 每個packet大小2048,2048/2(和下面采樣格式有關)=1024個采樣個數,1024/2(這里是雙通道)=512(是單通道)
                                        AV_SAMPLE_FMT_S16,    //采樣格式
                                        0);                    //對齊


    av_samples_alloc_array_and_samples(dst_data,            //前面兩個傳入地址,用作輸出;其他作為輸入數據
                                        dst_linesize,
                                        2,                    //AV_CH_LAYOUT_SURROUND 三通道
                                        512,                //輸出的單通道采樣個數,和上面一致即可,只是對每個采樣進行了數據轉換,對內部數據重采樣,沒有修改采樣個數
                                        AV_SAMPLE_FMT_S16,    //采樣格式
                                        0);                    //對齊
    return swr_cxt;
}

AVCodecContext* openCodec(){
        //---------1.打開編碼器
    AVCodec* codec = avcodec_find_encoder_by_name("libfdk_aac");    //內部要求的采樣大小就是是s16le------重點
    //---------2.創建上下文
    AVCodecContext* codec_ctx = avcodec_alloc_context3(codec);

    //------------------設置上下文參數
    codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;            //設置采樣格式(該字段是被固定了),上面說到,libfdk_aac處理16位,所以設置為16位。所以我們一般是將其他格式進行重采樣為16位
    codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;    //設置通道布局
    codec_ctx->channels = 2;                            //設置通道數(其實和上面一樣)
    codec_ctx->sample_rate = 44100;                        //設置采樣率
    codec_ctx->profile = FF_PROFILE_AAC_HE;            //設置AAC編碼格式,如果設置了這個字段,就不需要設置下面的比特率了
    //codec_ctx->bit_rate = 64000;                            //設置比特率,64k;對於每個編碼方式,都有最低編碼碼率。AAC_LC:128k AAC HE:64k AAC HE V2:32k

    //---------3.打開編碼器
    if(avcodec_open2(codec_ctx,codec,NULL)<0){
        av_log(NULL,AV_LOG_ERROR,"Failed to open libfdk_aac context\n");
        return NULL;
    }

    return codec_ctx;
}

void encode(AVCodecContext* codec_ctx,AVFrame* frame,AVPacket* newpkt,FILE* fp){
    //--------開始進行編碼操作---------重點
    int ret = avcodec_send_frame(codec_ctx,frame);    //將frame交給編碼器進行編碼;內部會將一幀數據掛到編碼器的緩沖區

    while(ret>=0){    //只有當frame被放入緩沖區之后(數據設置成功),並且下面ret表示獲取緩沖區數據完成后才退出循環。因為可能一個frame對應1或者多個packet,或者多個frame對應1個packet

        ret = avcodec_receive_packet(codec_ctx,newpkt);    //從編碼器中獲取編碼后的packet數據,處理多種情況

        if(ret<0){
            if(ret==AVERROR(EAGAIN)||ret==AVERROR_EOF){    //讀完數據
                return;
            }else{    //編碼器出錯
                av_log(NULL,AV_LOG_ERROR,"avcodec_receive_packet error! [%d] %s\n",ret,av_err2str(ret));
                return;
            }
        }

        int len = fwrite(newpkt->data,1,newpkt->size,fp);
        fflush(fp);
        
        if(len!=newpkt->size){
            av_log(NULL,AV_LOG_WARNING,"Warning,newpkt size:%d not equal writen size:%d\n",len,newpkt->size);
        }else{
            av_log(NULL,AV_LOG_INFO,"Success write newpkt to file\n");
        }
    }
}

AVFrame* initFrame(){
    AVFrame* frame = av_frame_alloc();    //分配frame空間,但是數據真正被存放在buffer中
    if(!frame){
        av_log(NULL,AV_LOG_ERROR,"Failed to create frame\n");
        return NULL;
    }

    frame->nb_samples = 512;            //單通道采樣數,和前面重采樣配置一樣
    frame->format = AV_SAMPLE_FMT_S16;    //采樣大小
    frame->channel_layout = AV_CH_LAYOUT_STEREO;
    av_frame_get_buffer(frame,0);        //第二個參數是對齊  512*2*2 = 2048
    if(!frame->data[0]){
        av_log(NULL,AV_LOG_ERROR,"Failed to create frame buffer\n");
        return NULL;
    }
    return frame;
}

void rec_audio(){
    char* devicename = "hw:0";
    char errors[1024];
    int ret,count=0,len;
    FILE* fp = NULL;

    AVFormatContext* fmt_ctx=NULL;    //格式上下文獲取-----av_read_frame獲取packet
    AVDictionary* options=NULL;
    AVInputFormat *iformat=NULL;
    AVPacket packet;    //包結構

    av_register_all();
    av_log_set_level(AV_LOG_DEBUG);
    //注冊所有的設備,包括我們需要的音頻設備
    avdevice_register_all();

    //獲取輸入(采集)格式
    iformat = av_find_input_format("alsa");

    //打開輸入設備
    ret = avformat_open_input(&fmt_ctx,devicename,iformat,&options);    //----打開輸入設備,初始化格式上下文和選項
    if(ret<0){
        av_strerror(ret,errors,1024);
        av_log(NULL,AV_LOG_ERROR,"Failed to open audio device,[%d]%s\n",ret,errors);
    }
    
    av_log(NULL,AV_LOG_INFO,"Success to open audio device\n");
    //打開文件
    fp = fopen("./audio_2.aac","wb");
    if(fp==NULL){
        av_log(NULL,AV_LOG_ERROR,"Failed to open out file\n");
        goto fail;
    }

    //創建重采樣的上下文
    SwrContext* swr_cxt = NULL;    //------根據initSwrCxt中的swr_alloc_set_opts創建重采樣上下文
    uint8_t** src_data = NULL;
    uint8_t** dst_data = NULL;
    int src_linesize = 0,dst_linesize=0;

    swr_cxt = initSwrCxt(&src_data,&src_linesize,&dst_data,&dst_linesize);

    if(!swr_cxt){
        av_log(NULL,AV_LOG_ERROR,"Failed to set swr context or initial swr context\n");
        goto fail;
    }


    //-------獲取編碼器上下文
    AVCodecContext* codec_ctx = openCodec();    //---------根據openCodec中的avcodec_alloc_context3分配編碼器的上下文
    if(!codec_ctx)
        goto fail;
    
    //-------設置輸入數據frame
    AVFrame* frame = initFrame();    //分配frame空間,但是數據真正被存放在buffer中
    if(!frame){
        av_log(NULL,AV_LOG_ERROR,"Failed to create frame\n");
        goto fail;
    }

    //-------設置輸入數據packet
    AVPacket* newpkt = av_packet_alloc();
    if(!newpkt){
        av_log(NULL,AV_LOG_ERROR,"Failed to create newpkt\n");
        goto fail;
    }

    //開始從設備中讀取數據
    while((ret=av_read_frame(fmt_ctx,&packet))==0&&count++<500){
        av_log(NULL,AV_LOG_INFO,"Packet size:%d(%p),cout:%d\n",packet.size,packet.data,count);

        //先對數據進行重采樣,然后寫入文件中
        memcpy((void*)src_data[0],(void*)packet.data,packet.size);

        swr_convert(swr_cxt,                                //上下文
                    dst_data,                                //輸出數組(雙指針)
                    512,                                    //每個通道的采樣數
                    (const uint8_t**)src_data,                //輸入數據,來自與packet.data,要改造格式
                    512);                                    //輸入的通道采樣數

        //--------將重采樣后的數據轉存入frame中去
        memcpy((void*)frame->data[0],dst_data[0],dst_linesize);
        //將編碼數據寫入文件
        encode(codec_ctx,frame,newpkt,fp);

        //釋放空間
        av_packet_unref(&packet);
    }
    //---------強制將編碼器緩沖區中的音頻數據進行編碼輸出
    encode(codec_ctx,NULL,newpkt,fp);
fail:

    if(fp)
        fclose(fp);

    //釋放空間
    if(src_data){
        av_freep(&src_data[0]);
    }
    av_freep(&src_data);

    if(dst_data){
        av_freep(&dst_data[0]);
    }
    av_freep(&dst_data);

    //釋放重采樣上下文
    swr_free(&swr_cxt);
    //釋放packet和frame
    if(frame){
        av_frame_free(&frame);
    }

    if(newpkt){
        av_packet_free(&newpkt);
    }
    //釋放編碼器上下文
    if(codec_ctx){
        avcodec_free_context(&codec_ctx);
    }
    
    //關閉設備、釋放上下文空間
    avformat_close_input(&fmt_ctx);
    return ;
}

int main(int argc,char* argv)
{
    rec_audio();
    return 0;
}
View Code
gcc -o ce 05CreateEncoder.c -I /usr/local/ffmpeg/include/ -L /usr/local/ffmpeg/lib/ -lavutil -lavformat -lavcodec -lavdevice -lswresample

可以發現,通過編碼后的數據播放時的參數Audio: aac(HE-AAC)是經過AAC編碼的,不同於六中的PCM原始數據!!!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM