首页 > 其他分享 >15_AAC编码实战

15_AAC编码实战

时间:2022-10-05 20:00:24浏览次数:80  
标签:编码 15 AAC aac frame ctx ret libfdk

本文将分别通过命令行、编程2种方式进行AAC编码实战,使用的编码库是libfdk_aac。

要求

fdk-aac对输入的PCM数据是有参数要求的,如果参数不对,就会出现以下错误:

[libfdk_aac @ 0x7fa3db033000] Unable to initialize the encoder: SBR library initialization error
Error initializing output stream 0:0 -- Error while opening encoder for output stream #0:0 - maybe incorrect parameters such as bit_rate, rate, width or height
Conversion failed!

采样格式

必须是16位整数PCM。

采样率

支持的采样率有(Hz):

  • 8000、11025、12000、16000、22050、24000、32000
  • 44100、48000、64000、88200、96000

命令行

基本使用

最简单的用法如下所示:

# pcm -> aac
ffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac

# wav -> aac
# 为了简化指令,本文后面会尽量使用in.wav取代in.pcm
ffmpeg -i in.wav -c:a libfdk_aac out.aac
  • -ar 44100 -ac 2 -f s16le

    • PCM输入数据的参数
  • -c:a

    • 设置音频编码器
    • c表示codec(编解码器),a表示audio(音频)
    • 等价写法
      • -codec:a
      • -acodec
    • 需要注意的是:这个参数要写在aac文件那边,也就是属于输出参数

默认生成的aac文件是LC规格的。

ffprobe out.aac

# 输出结果如下所示
Audio: aac (LC), 44100 Hz, stereo, fltp, 120 kb/s

常用参数

  • -b:a
    • 设置输出比特率
    • 比如*-b:a 96k*
ffmpeg -i in.wav -c:a libfdk_aac -b:a 96k out.aac
  • -profile:a
    • 设置输出规格
    • 取值有:
      • aac_low:Low Complexity AAC (LC),默认值
      • aac_he:High Efficiency AAC (HE-AAC)
      • aac_he_v2:High Efficiency AAC version 2 (HE-AACv2)
      • aac_ld:Low Delay AAC (LD)
      • aac_eld:Enhanced Low Delay AAC (ELD)
    • 一旦设置了输出规格,会自动设置一个合适的输出比特率
      • 也可以用过*-b:a*自行设置输出比特率
ffmpeg -i in.wav -c:a libfdk_aac -profile:a aac_he_v2 -b:a 32k out.aac
  • -vbr
    • 开启VBR模式(Variable Bit Rate,可变比特率)
    • 如果开启了VBR模式,-b:a选项将会被忽略,但*-profile:a*选项仍然有效
    • 取值范围是0 ~ 5
      • 0:默认值,关闭VBR模式,开启CBR模式(Constant Bit Rate,固定比特率)
      • 1:质量最低(但是音质仍旧很棒)
      • 5:质量最高
VBR kbps/channel AOTs
1 20-32 LC、HE、HEv2
2 32-40 LC、HE、HEv2
3 48-56 LC、HE、HEv2
4 64-72 LC
5 96-112 LC

AOT是Audio Object Type的简称。

ffmpeg -i in.wav -c:a libfdk_aac -vbr 1 out.aac

文件格式

我曾在《重识音频》中提到,AAC编码的文件扩展名主要有3种:aac、m4a、mp4。

# m4a
ffmpeg -i in.wav -c:a libfdk_aac out.m4a

# mp4
ffmpeg -i in.wav -c:a libfdk_aac out.mp4

编程

AAC 编码流程:
aac编码流程

需要用到2个库:

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/avutil.h>
}

// 错误处理
#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

函数声明

我们最终会将PCM转AAC的操作封装到一个函数中。

extern "C" {
#include <libavcodec/avcodec.h>
}

// 参数
typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioEncodeSpec;

class FFmpegUtil {
public:
    FFmpegUtil();

    static void aacEncode(AudioEncodeSpec &in,
                          const char *outFilename);
};

函数实现

变量定义

// 编码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;

// 用来存放编码前的数据
AVFrame *frame = nullptr;
// 用来存放编码后的数据
AVPacket *pkt = nullptr;

// 返回结果
int ret = 0;

// 输入文件
QFile inFile(in.filename);
// 输出文件
QFile outFile(outFilename);

获取编码器

下面的代码可以获取FFmpeg默认的AAC编码器(并不是libfdk_aac)。

AVCodec *codec1 = avcodec_find_encoder(AV_CODEC_ID_AAC);

AVCodec *codec2 = avcodec_find_encoder_by_name("aac");

// true
qDebug() << (codec1 == codec2);

// aac
qDebug() << codec1->name;

不过我们最终要获取的是libfdk_aac。

// 获取fdk-aac编码器
codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
    qDebug() << "encoder libfdk_aac not found";
    return;
}

检查采样格式

接下来检查编码器是否支持当前的采样格式。

// 检查采样格式
if (!check_sample_fmt(codec, in.sampleFmt)) {
    qDebug() << "Encoder does not support sample format"
             << av_get_sample_fmt_name(in.sampleFmt);
    return;
}

检查函数check_sample_fmt的实现如下所示。

// 检查编码器codec是否支持采样格式sample_fmt
static int check_sample_fmt(const AVCodec *codec,
                            enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;
    while (*p != AV_SAMPLE_FMT_NONE) {
        if (*p == sample_fmt) return 1;
        p++;
    }
    return 0;
}

创建上下文

avcodec_alloc_context3后面的3说明这已经是第3版API,取代了此前的avcodec_alloc_contextavcodec_alloc_context2

// 创建上下文
ctx = avcodec_alloc_context3(codec);
if (!ctx) {
    qDebug() << "avcodec_alloc_context3 error";
    return;
}

// 设置参数
ctx->sample_fmt = in.sampleFmt;
ctx->sample_rate = in.sampleRate;
ctx->channel_layout = in.chLayout;
// 比特率
ctx->bit_rate = 32000;
// 规格
ctx->profile = FF_PROFILE_AAC_HE_V2;

打开编码器

// 打开编码器
ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "avcodec_open2 error" << errbuf;
    goto end;
}

如果是想设置一些libfdk_aac特有的参数(比如vbr),可以通过options参数传递。

AVDictionary *options = nullptr;
av_dict_set(&options, "vbr", "1", 0);
ret = avcodec_open2(ctx, codec, &options);

创建AVFrame

AVFrame用来存放编码前的数据。

// 创建AVFrame
frame = av_frame_alloc();
if (!frame) {
    qDebug() << "av_frame_alloc error";
    goto end;
}

// 样本帧数量(由frame_size决定)
frame->nb_samples = ctx->frame_size;
// 采样格式
frame->format = ctx->sample_fmt;
// 声道布局
frame->channel_layout = ctx->channel_layout;
// 创建AVFrame内部的缓冲区
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
    ERROR_BUF(ret);
    qDebug() << "av_frame_get_buffer error" << errbuf;
    goto end;
}

创建AVPacket

// 创建AVPacket
pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "av_packet_alloc error";
    goto end;
}

打开文件

// 打开文件
if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "file open error" << in.filename;
    goto end;
}
if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "file open error" << outFilename;
    goto end;
}

开始编码

// frame->linesize[0]是缓冲区的大小
// 读取文件数据
while ((ret = inFile.read((char *) frame->data[0],
                          frame->linesize[0])) > 0) {
    // 最后一次读取文件数据时,有可能并没有填满frame的缓冲区
    if (ret < frame->linesize[0]) {
        // 声道数
        int chs = av_get_channel_layout_nb_channels(frame->channel_layout);
        // 每个样本的大小
        int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
        // 改为真正有效的样本帧数量
        frame->nb_samples = ret / (chs * bytes);
    }

    // 编码
    if (encode(ctx, frame, pkt, outFile) < 0) {
        goto end;
    }
}

// flush编码器
encode(ctx, nullptr, pkt, outFile);

encode函数专门用来进行编码,它的实现如下所示。

// 音频编码
// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
static int encode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {
    // 发送数据到编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    while (true) {
        // 从编码器中获取编码后的数据
        ret = avcodec_receive_packet(ctx, pkt);
        // packet中已经没有数据,需要重新发送数据到编码器(send frame)
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) { // 出现了其他错误
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_packet error" << errbuf;
            return ret;
        }

        // 将编码后的数据写入文件
        outFile.write((char *) pkt->data, pkt->size);

        // 释放资源
        av_packet_unref(pkt);
    }

    return 0;
}

资源回收

end:
    // 关闭文件
    inFile.close();
    outFile.close();

    // 释放资源
    av_frame_free(&frame);
    av_packet_free(&pkt);
    avcodec_free_context(&ctx);

函数调用

#ifdef Q_OS_WIN
    // PCM文件的文件名
    #define IN_FILENAME "../test/44100_s16le_2.pcm"
    #define OUT_FILENAME "../test/out.aac"
#else
    #define IN_FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/44100_s16le_2.pcm"
    #define OUT_FILENAME "/Users/zuojie/QtProjects/audio-video-dev/test/out.acc"
#endif

AudioEncodeSpec in;
in.filename = IN_FILENAME;
in.sampleFmt = AV_SAMPLE_FMT_S16;
in.sampleRate = 44100;
in.chLayout = AV_CH_LAYOUT_STEREO;

FFmpegUtil::aacEncode(in,OUT_FILENAME);

注意

上面的开始编码步骤的while循环里最开始没有下面的代码运行代码生成out1.aac文件

// 最后一次读取文件数据时,有可能并没有填满frame的缓冲区
if (ret < frame->linesize[0]) {
    // 声道数
    int chs = av_get_channel_layout_nb_channels(frame->channel_layout);
    // 每个样本的大小
    int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
    // 改为真正有效的样本帧数量
    frame->nb_samples = ret / (chs * bytes);
}

然后我们在使用ffmpeg命令方式生成out2.aac文件

ffmpeg -ar 44100 -ac 2 -f s16le -i 44100_s16le_2.pcm -c:a libfdk_aac -b:a 32k -profile:a aac_he_v2 out2.aac


可以发现代码中生成的和ffmpeg命令生成的多5个字节,这是怎么回事呢? 这是因为,在读取pcm文件的时候,当最后一次读取的时候填不满AVFrame缓存区,例如缓存区大小是4096字节,但是最后一次读取pcm文件可能是1024字节,没法填满缓冲区的4096字节,因此在送入编码器的时候,编码器直接把缓冲区的4096全部进行编码,就会导致多余一些无效字节。

代码链接

标签:编码,15,AAC,aac,frame,ctx,ret,libfdk
From: https://www.cnblogs.com/zuojie/p/16756243.html

相关文章

  • 13_AAC编码介绍
    AAC(AdvancedAudioCoding,译为:高级音频编码),是由FraunhoferIIS、杜比实验室、AT&T、Sony、Nokia等公司共同开发的有损音频编码和文件格式。对比MP3AAC被设计为MP3格式......
  • luogu P3644 [APIO2015] 八邻旁之桥
    Link题解首先忽略掉同侧的询问。对于\(K=1\),它其实就是问一个点到其它点的距离之和最小值,直接找到中位数然后计算即可。对于一条路线,我们可以发现,如果建的桥里这两个......
  • 字符编码 XUTF
    /**Copyright(c)HuaweiTechnologiesCo.,Ltd.2019-2020.Allrightsreserved.*Description:上机编程认证*Note:缺省代码仅供参考,可自行决定使用、修改或删除......
  • 【题解】P3583 [POI2015] KWA
    模拟赛出这道题???还好赛时乱搞做出来了(/hanxlinkDescription定义一个数\(n\)的拆分为:将\(n\)表示为若干个不同的正整数的平方和。令\(k(n)\)为\(n\)的拆分中最......
  • 20146月份到2015年5月份70个大中城市住宅销售价格变动情况
    2015年5月份70个大中城市住宅销售价格变动情况​​​http://www.stats.gov.cn/tjsj/zxfb/201506/t20150618_1170358.html​​​(一)与上月相比,70个大中城市中,价格下降的......
  • 【笨方法学python】ex15 - 读取文件
    代码如下:点击查看代码#-*-coding:utf-8--*-#读取文件fromsysimportargvscript,filename=argvtxt=open(filename)print"Here'syourfile%r:"%f......
  • 代码随想录训练营|Day 15|102, 226, 101
    102.BinaryTreeLevelOrderTraversalGiventhe root ofabinarytree,return thelevelordertraversalofitsnodes'values.(i.e.,fromlefttoright,le......
  • 代码随想录day15 | 102.二叉树的层序遍历 226.反转二叉树 101.对称二叉树
    102.二叉树的层序遍历题目|文章1.迭代思路1.创建一个队列2.确定每一层的节点个数,对每一层进行遍历,将结果输出。实现点击查看代码classSolution{public:ve......
  • Demo15_输出1-100且输出1+2+3+.....+100的值
    //输出1-100的方法,while:就是一个循环结构的语句packagecom.HuanXin.JiBen_JieGou;publicclassDemo07_While{publicstaticvoidmain(String[]args){int......
  • Demo15_输出1-100
    ackagecom.HuanXin.JiBen_JieGou;publicclassDemo07_While1{publicstaticvoidmain(String[]args){//求出1+2+3+4++100=?intA=0;int......