15_AAC编码实战
本文将分别通过命令行、编程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 编码流程:

需要用到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_context和avcodec_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编码实战的更多相关文章
- 《Java8 Stream编码实战》正式推出
当我第一次在项目代码中看到Stream流的时候,心里不由得骂了一句"傻X"炫什么技.当我开始尝试在代码中使用Stream时,不由得感叹真香. 记得以前有朋友聊天说,他在代码中用了 ...
- 超详细的编码实战,让你的springboot应用识别图片中的行人、汽车、狗子、喵星人(JavaCV+YOLO4)
欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...
- Web自动化之Headless Chrome编码实战
API 概览 && 编码Tips 文档地址 github Chrome DevTools Protocol 协议本身的仓库 有问题可以在这里提issue github debugger ...
- 长篇图解etcd核心应用场景及编码实战
大家好啊,我是字母哥,今天写一篇关于etcd的文章,其实网上也有很多关于etcd的介绍,我就简明扼要,总结提炼,期望大家通过这一篇文章掌握etcd的核心知识以及编码技能! 本文首先用大白话给大家介绍一 ...
- 学习笔记_Java_day14—编码实战___一个注册页面的完整流程
- 编码实战Web端联系人的增删改查
首先画出分析图 实现效果如图 项目下的包如图: 实体包 package com.contactSystem.entiey; public class Contact { private String ...
- day14(编码实战-用户登录注册)
day14 案例:用户注册登录 要求:3层框架,使用验证码 功能分析 注册 登录 1.1 JSP页面 regist.jsp 注册表单:用户输入注册信息: 回显错误信息:当注册失败时,显示错误信 ...
- 安卓topbar编码实战
1.先在res->value下新建attrs.xml文件 <?xml version="1.0" encoding="utf-8"?> < ...
- Scala高手实战****第18课:Scala偏函数、异常、Lazy值编码实战及Spark源码鉴赏
本篇文章主要讲述Scala函数式编程之偏函数,异常,及Lazy 第一部分:偏函数 偏函数:当函数有多个参数,而在使用该函数时不想提供所有参数(比如函数有3个参数),只提供0~2个参数,此时得到的函数便 ...
- JVM内存结构探秘及编码实战
了解JVM内存结构的目的 在Java的开发过程中,因为有JVM自动内存管理机制,不再需要像在C.C++开发那样手动释放对象的内存空间,不容易出现内存泄漏和内存溢出的问题.但是,正是由于把内存管理的权利 ...
随机推荐
- IIS创建和管理虚拟网站
实验介绍: 本文会详细介绍创建虚拟站点的三种方法 一:IP地址建立站点 1.打开安装了IIS的windows,进入ip配置页面. 添加几个ip,我这里添加的是192.168.1.209,192.168 ...
- Argocd学习
argocd官网文档链接 ArgoCD官网文档 在K8S集群使用argocd命令将集群添加到argcd的cluster列表中 argocd cluster add kubernetes-admin@i ...
- Ubuntu安装PHP和NGINX环境
Ubuntu安装PHP和NGINX环境 介绍 PHP-FPM PHP-FPM 是 PHP FastCGI Process Manager 的缩写,是 FastCGI 进程管理器. PHP-FPM 是基 ...
- JS Leetcode 496. 下一个更大元素 I 更清晰的图解单调栈做法
壹 ❀ 引 最近一周的工作压力很大...一周的时间一直在处理一个APP漏洞问题,因为项目三年无人维护,突然要改东西光是修改构建错误以及三方包依赖错误就花了三天时间= =.不过好在问题到已经结束尾,闲下 ...
- PyOCD Notes
Installation Ubuntu20.04 For Ubuntu20.04 the version in apt repository is 0.13.1+dfsg-1, which is to ...
- Java并发编程实例--10.使用线程组
并发API提供的一个有趣功能是可以将多个线程组成一个组. 这样我们就能将这一组线程看做一个单元并且提供改组内线程对象的读取操作.例如 你有一些线程在执行同样的任务并且你想控制他们,不考虑有多少个线程仍 ...
- Puppeteer介绍
Puppeteer是什么 Puppeteer是一个Node库,它提供了一个高级API来通过DevTools协议控制Chromium或Chrome. 可以使用Puppeteer来自动化完成浏览器的操作, ...
- Docker实践之07-数据管理
目录 一.数据卷概述 二.创建数据卷 三.查看数据卷 四.挂载数据卷 五.删除数据卷 六.挂载主机目录或文件 七.挂载数据卷与主机目录/文件的比较 一.数据卷概述 数据卷是一个可供一个或多个容器使用的 ...
- 运用 Argo Workflows 协调 CI/CD 流水线
Argo Workflows 是一个开源的容器原生工作流引擎,用于协调 CI/CD 在 Kubernetes 中的运作.它以 Kubernetes 自定义资源(CRD)的形式实现,使开发人员能够创建自 ...
- 基于javaweb的服装租赁网站
演示 技术+环境+工具 jdk8+maven.3.2.1+mysql5.7+idea+navicat+spring+springmvc+mybatis+bootstrap+jquery+ajax