使用FFmpeg API编码器生成AAC音频文件

解题思路:

前文写到了使用API接口将生成的纯音PCM样本直接写入到.mp3文件中,我们是否可以使用同样的方法生成.aac文件呢?
答案是不行,AAC文件格式要求写入相应的头部,这一块比较复杂,具体请百度参阅相关文档。

下面抄自网络:

有的时候当你编码AAC裸流的时候,会遇到写出来的AAC文件并不能在PC和手机上播放,很大的可能就是AAC文件的每一帧里缺少了ADTS头信息文件的包装拼接。只需要加入头文件ADTS即可。一个AAC原始数据块长度是可变的,对原始帧加上ADTS头进行ADTS的封装,就形成了ADTS帧。
AAC音频文件的每一帧由ADTS Header和AAC Audio Data组成。结构体如下:


image.png

简单可行的方法是,我们使用AVFormat的封装器,由封装器帮我们做各种构包的复杂工作。下面是我从网上抄的图,它很好的演示了API的调用路线:


image.png

相关接口:

补充本文引入的封装器操作相关的结构和函数。

结构介绍:

封装与解封装上下文:

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;
};

编解码器参数结构:

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);
};
函数介绍:

分配输出流上下文,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);
根据s指定的上下文,写入文件头。
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
根据s指定的上下文,写入压缩包。
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
根据s指定的上下文,写入文件尾。
int av_write_trailer(AVFormatContext *s);

创建一个新的流。
AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);
拷贝编解码器的参数到流参数中。
int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);

打开媒体文件,url可以是磁盘文件,也可以是rtmp地址。
int avio_open(AVIOContext **s, const char *url, int flags);
关闭媒体文件。
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;
    }

    // 根据编码器初使化编码器上下文结构,并设置相关默认值 
    AVCodecContext* pCodecCTX = avcodec_alloc_context3(pCodec);
    if (pCodecCTX == NULL)
    {
        printf("alloc aac encoder context failed! \n");
        return 0;
    }

    // 填写音频编码的关键参数:样本格式、通道数及布局、采样率
    pCodecCTX->sample_fmt = AV_SAMPLE_FMT_FLTP;
    pCodecCTX->channel_layout = AV_CH_LAYOUT_STEREO;
    pCodecCTX->channels = 2;
    pCodecCTX->sample_rate = 44100;

    // 以设定的参数打开编码器
    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();

    // 设置音频帧的相关参数:样本格式、通道数及布局、样本数量
    pFrame->format = pCodecCTX->sample_fmt;
    pFrame->channel_layout = pCodecCTX->channel_layout;
    pFrame->channels = pCodecCTX->channels;
    pFrame->nb_samples = pCodecCTX->frame_size;

    // 根据参数设置,申请帧空间
    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;
    }

    // 创建一路输出流
    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;
    }

    // 当前已经处理的包数
    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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,271评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,725评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,252评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,634评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,549评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,985评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,471评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,128评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,257评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,233评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,235评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,940评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,528评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,623评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,858评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,245评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,790评论 2 339

推荐阅读更多精彩内容