使用FFmpeg API編碼器生成AAC音頻文件

解題思路:

前文寫到了使用API接口將生成的純音PCM樣本直接寫入到.mp3文件中睹欲,我們是否可以使用同樣的方法生成.aac文件呢惕蹄?
答案是不行记舆,AAC文件格式要求寫入相應(yīng)的頭部箫荡,這一塊比較復(fù)雜说榆,具體請百度參閱相關(guān)文檔锥忿。

下面抄自網(wǎng)絡(luò):

有的時候當你編碼AAC裸流的時候牛郑,會遇到寫出來的AAC文件并不能在PC和手機上播放,很大的可能就是AAC文件的每一幀里缺少了ADTS頭信息文件的包裝拼接敬鬓。只需要加入頭文件ADTS即可淹朋。一個AAC原始數(shù)據(jù)塊長度是可變的笙各,對原始幀加上ADTS頭進行ADTS的封裝,就形成了ADTS幀础芍。
AAC音頻文件的每一幀由ADTS Header和AAC Audio Data組成杈抢。結(jié)構(gòu)體如下:


image.png

簡單可行的方法是,我們使用AVFormat的封裝器仑性,由封裝器幫我們做各種構(gòu)包的復(fù)雜工作惶楼。下面是我從網(wǎng)上抄的圖,它很好的演示了API的調(diào)用路線:


image.png

相關(guān)接口:

補充本文引入的封裝器操作相關(guān)的結(jié)構(gòu)和函數(shù)诊杆。

結(jié)構(gòu)介紹:

封裝與解封裝上下文:

typedef struct AVFormatContext
{
const struct AVInputFormat *iformat;
const struct AVOutputFormat *oformat;
AVIOContext *pb;
unsigned int nb_streams;
AVStream **streams;
int64_t start_time;
int64_t duration;
int64_t bit_rate;
AVDictionary *metadata;
} AVFormatContext;

媒體流的操作上下文:

typedef struct AVStream 
{
    int index;    /**< stream index in AVFormatContext */
    AVRational time_base;
    int64_t start_time;
    int64_t duration;
    int64_t nb_frames;
    AVRational r_frame_rate;
    AVRational avg_frame_rate;
    AVDictionary *metadata;
    AVCodecParameters *codecpar;
};

編解碼器參數(shù)結(jié)構(gòu):

struct AVCodecParameters 
{
    enum AVMediaType codec_type;
    enum AVCodecID   codec_id;
    uint32_t         codec_tag;

    /**
     * - video: the pixel format, the value corresponds to enum AVPixelFormat.
     * - audio: the sample format, the value corresponds to enum AVSampleFormat.
     */
    int format;
    /**
     * The average bitrate of the encoded data (in bits per second).
     */
    int64_t bit_rate;

    int profile;
    int level;

    int width;
    int height;

    uint64_t channel_layout;
    int      channels;
    int      sample_rate;
};

媒體文件直接讀寫操作上下文:

typedef struct AVIOContext
{
    unsigned char *buffer;  /**< Start of the buffer. */
    int buffer_size;  /**< Maximum buffer size */
    unsigned char *buf_ptr; /**< Current position in the buffer */
    unsigned char *buf_end; /**< End of the data, may be less than
    void *opaque;  /**< A private pointer, passed to the read/write/seek/...functions. */
    int (*read_packet)(void *opaque, uint8_t *buf, int buf_size);
    int (*write_packet)(void *opaque, uint8_t *buf, int buf_size);
    int64_t (*seek)(void *opaque, int64_t offset, int whence);
};
函數(shù)介紹:

分配輸出流上下文歼捐,format_name指定輸出文件格式,當format_name為NULL時晨汹,由filename的擴展名決定類型豹储。
int avformat_alloc_output_context2(AVFormatContext **ctx, const AVOutputFormat *oformat, const char *format_name, const char *filename);
釋放分配輸出流上下文。
void avformat_free_context(AVFormatContext *s);
根據(jù)s指定的上下文淘这,寫入文件頭剥扣。
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
根據(jù)s指定的上下文,寫入壓縮包慨灭。
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
根據(jù)s指定的上下文朦乏,寫入文件尾。
int av_write_trailer(AVFormatContext *s);

創(chuàng)建一個新的流氧骤。
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
拷貝編解碼器的參數(shù)到流參數(shù)中呻疹。
int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);

打開媒體文件,url可以是磁盤文件筹陵,也可以是rtmp地址刽锤。
int avio_open(AVIOContext **s, const char *url, int flags);
關(guān)閉媒體文件。
int avio_closep(AVIOContext **s);

代碼舉例:

代碼的邏輯基本和前文生成MP3的一樣朦佩,主要加入了封裝器操作并思,如下:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

extern "C"
{
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libavutil/channel_layout.h>
}

// 編碼幀
bool encode(AVCodecContext* pCodecCTX, const AVFrame* pFrame, AVPacket* pPacket, AVFormatContext* pOutputCTX, int& nPacketCount);

int main(int argc, char* argv[])
{
    // 搜索指定的編碼器
    const AVCodec* pCodec = avcodec_find_encoder_by_name("aac");
    if (pCodec == NULL)
    {
        printf("not support aac encoder! \n");
        return 0;
    }

    // 以下打印該編碼器支持的樣本格式、通道布局语稠、采樣率

    printf("support sample formats: \n");
    const enum AVSampleFormat* pSampleFMT = pCodec->sample_fmts;
    while (pSampleFMT && *pSampleFMT)
    {
        printf("\t %d - %s \n", *pSampleFMT, av_get_sample_fmt_name(*pSampleFMT));
        ++pSampleFMT;
    }

    printf("support layouts: \n");
    const uint64_t* pLayout = pCodec->channel_layouts;
    while (pLayout && *pLayout)
    {
        int nb_channels = av_get_channel_layout_nb_channels(*pLayout);

        char sBuf[128] = {0};
        av_get_channel_layout_string(sBuf, sizeof(sBuf), nb_channels, *pLayout);

        printf("\t %d - %s \n", nb_channels, sBuf);
        ++pLayout;
    }

    printf("support sample rates: \n");
    const int* pSampleRate = pCodec->supported_samplerates;
    while (pSampleRate && *pSampleRate)
    {
        printf("\t %dHz \n", *pSampleRate);
        ++pSampleRate;
    }

    // 根據(jù)編碼器初使化編碼器上下文結(jié)構(gòu)宋彼,并設(shè)置相關(guān)默認值 
    AVCodecContext* pCodecCTX = avcodec_alloc_context3(pCodec);
    if (pCodecCTX == NULL)
    {
        printf("alloc aac encoder context failed! \n");
        return 0;
    }

    // 填寫音頻編碼的關(guān)鍵參數(shù):樣本格式、通道數(shù)及布局仙畦、采樣率
    pCodecCTX->sample_fmt = AV_SAMPLE_FMT_FLTP;
    pCodecCTX->channel_layout = AV_CH_LAYOUT_STEREO;
    pCodecCTX->channels = 2;
    pCodecCTX->sample_rate = 44100;

    // 以設(shè)定的參數(shù)打開編碼器
    int rc = avcodec_open2(pCodecCTX, pCodec, NULL);
    if (rc < 0)
    {
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avcodec_open2() ret:[%d:%s] \n", rc, sError);
        return -1;
    }

    AVPacket* pPacket = av_packet_alloc();
    AVFrame* pFrame = av_frame_alloc();

    // 設(shè)置音頻幀的相關(guān)參數(shù):樣本格式输涕、通道數(shù)及布局、樣本數(shù)量
    pFrame->format = pCodecCTX->sample_fmt;
    pFrame->channel_layout = pCodecCTX->channel_layout;
    pFrame->channels = pCodecCTX->channels;
    pFrame->nb_samples = pCodecCTX->frame_size;

    // 根據(jù)參數(shù)設(shè)置慨畸,申請幀空間
    rc = av_frame_get_buffer(pFrame, 0);
    if (rc < 0)
    {   
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avcodec_open2() ret:[%d:%s] \n", rc, sError);
        return -1;
    }

    // 這里打開輸出文件
    AVFormatContext* pOutputCTX = NULL;
    rc = avformat_alloc_output_context2(&pOutputCTX, NULL, NULL, "test.aac");
    if (rc < 0)
    {   
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avformat_alloc_output_context2() ret:[%d:%s] \n", rc, sError);
        return -1;
    }

    // 創(chuàng)建一路輸出流
    AVStream* pStreamOut = avformat_new_stream(pOutputCTX, NULL);
    avcodec_parameters_from_context(pStreamOut->codecpar, pCodecCTX);

    // 打開輸出文件
    rc = avio_open(&pOutputCTX->pb, "test.aac", AVIO_FLAG_WRITE);
    if (rc < 0)
    {   
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avio_open() ret:[%d:%s] \n", rc, sError);
        return -1;
    }

    // 寫入輸出文件頭
    rc = avformat_write_header(pOutputCTX, NULL);
    if (rc < 0)
    {   
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avformat_write_header() ret:[%d:%s] \n", rc, sError);
        return -1;
    }

    // 當前已經(jīng)處理的包數(shù)
    int nPacketCount = 0;

    // 計算1Khz的正弦波采樣步進
    float fXStep = 2 * 3.1415926 * 1000 / pCodecCTX->sample_rate;

    for (int n = 0; n < pCodecCTX->sample_rate * 10; )
    {
        av_frame_make_writable(pFrame);

        // 下面以平面格式進行音頻格式組裝
        for (int c = 0; c < pFrame->channels; ++c)
        {
            float* pData = (float*)pFrame->data[c];

            for (int i = 0; i < pFrame->nb_samples; ++i)
            {
                pData[i] = sin(fXStep * (n + i)) * 1000;
            }
        }

        // 編碼生成壓縮格式
        if (!encode(pCodecCTX, pFrame, pPacket, pOutputCTX, nPacketCount))
        {
            printf("encode() fatal! \n");
            exit(-1);
        }

        n += pFrame->nb_samples;
    }

    // 寫入尾包
    encode(pCodecCTX, NULL, pPacket, pOutputCTX, nPacketCount);

    av_write_trailer(pOutputCTX);

    printf("test.aac write succuss! \n");

    avio_closep(&pOutputCTX->pb);
    avformat_free_context(pOutputCTX);

    av_frame_free(&pFrame);
    av_packet_free(&pPacket);
    avcodec_free_context(&pCodecCTX);

    return 0;
}

// 編碼幀
bool encode(AVCodecContext* pCodecCTX, const AVFrame* pFrame, AVPacket* pPacket, AVFormatContext* pOutputCTX, int& nPacketCount)
{
    int rc = avcodec_send_frame(pCodecCTX, pFrame);
    if (rc < 0)
    {
        char sError[128] = {0};
        av_strerror(rc, sError, sizeof(sError));
        printf("avcodec_send_frame() ret:[%d:%s] \n", rc, sError);

        return false;
    }

    while (true) 
    {
        rc = avcodec_receive_packet(pCodecCTX, pPacket);
        if (rc < 0)
        {
            if (rc == AVERROR(EAGAIN) || rc == AVERROR_EOF)
                return true;
                
            char sError[128] = {0};
            av_strerror(rc, sError, sizeof(sError));
            printf("avcodec_receive_packet() ret:[%d:%s] \n", rc, sError);

            return false;
        }

        // 指定流索引
        pPacket->stream_index = 0;

        // 對音頻來說莱坎,簡單補充PTS、DTS
        pPacket->dts = pPacket->pts = nPacketCount++;

        av_interleaved_write_frame(pOutputCTX, pPacket);
        av_packet_unref(pPacket);
    }

    return true;
}

編譯:
g++ -o encode_aac encode_aac.cpp -I/usr/local/ffmpeg/include -L/usr/local/ffmpeg/lib -lavformat -lavcodec -lavutil

運行寸士,輸出如下:

$ ./encode_aac
support sample formats:
         8 - fltp
         -1 - (null)
         3145826 - (null)
support layouts:
support sample rates:
         96000Hz
         88200Hz
         64000Hz
         48000Hz
         44100Hz
         32000Hz
         24000Hz
         22050Hz
         16000Hz
         12000Hz
         11025Hz
         8000Hz
         7350Hz
test.aac write succuss!
[aac @ 0000026156518500] Qavg: 1729.335

如果輸出如下如下的信息:
[adts @ 000002126e08cb40] Encoder did not produce proper pts, making some up.
這里表示寫入AAC文件時要求我們填寫PTS檐什,但對音頻來說碴卧,PTS和DTS并不要求絕對正確,通常只要做到自增即可乃正。

查看test.aac媒體信息:

$ ffprobe -i test.aac
ffprobe version N-104465-g08a501946f Copyright (c) 2007-2021 the FFmpeg developers
  built with gcc 11.2.0 (Rev6, Built by MSYS2 project)
  configuration: --prefix=/usr/local/ffmpeg --enable-shared --enable-libmp3lame
  libavutil      57.  7.100 / 57.  7.100
  libavcodec     59. 12.100 / 59. 12.100
  libavformat    59.  8.100 / 59.  8.100
  libavdevice    59.  0.101 / 59.  0.101
  libavfilter     8. 16.101 /  8. 16.101
  libswscale      6.  1.100 /  6.  1.100
  libswresample   4.  0.100 /  4.  0.100
[aac @ 0000029e6d19cbc0] Estimating duration from bitrate, this may be inaccurate
Input #0, aac, from 'test.aac':
  Duration: 00:00:10.09, bitrate: 129 kb/s
  Stream #0:0: Audio: aac (LC), 44100 Hz, stereo, fltp, 129 kb/s
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末住册,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烫葬,更是在濱河造成了極大的恐慌界弧,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搭综,死亡現(xiàn)場離奇詭異垢箕,居然都是意外死亡,警方通過查閱死者的電腦和手機兑巾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門条获,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蒋歌,你說我怎么就攤上這事帅掘。” “怎么了堂油?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵修档,是天一觀的道長。 經(jīng)常有香客問我府框,道長吱窝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任迫靖,我火速辦了婚禮院峡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘系宜。我一直安慰自己照激,他們只是感情好,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布盹牧。 她就那樣靜靜地躺著俩垃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪汰寓。 梳的紋絲不亂的頭發(fā)上吆寨,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天,我揣著相機與錄音踩寇,去河邊找鬼。 笑死六水,一個胖子當著我的面吹牛俺孙,可吹牛的內(nèi)容都是我干的辣卒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼睛榄,長吁一口氣:“原來是場噩夢啊……” “哼荣茫!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起场靴,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤啡莉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后旨剥,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咧欣,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年轨帜,在試婚紗的時候發(fā)現(xiàn)自己被綠了魄咕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡蚌父,死狀恐怖哮兰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苟弛,我是刑警寧澤喝滞,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站膏秫,受9級特大地震影響右遭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜荔睹,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一狸演、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧僻他,春花似錦宵距、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至劝篷,卻和暖如春哨鸭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背娇妓。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工像鸡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人哈恰。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓只估,卻偏偏與公主長得像志群,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛔钙,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容