本文将分别通过命令行、编程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_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编码实战的更多相关文章

  1. 《Java8 Stream编码实战》正式推出

    ​当我第一次在项目代码中看到Stream流的时候,心里不由得骂了一句"傻X"炫什么技.当我开始尝试在代码中使用Stream时,不由得感叹真香. 记得以前有朋友聊天说,他在代码中用了 ...

  2. 超详细的编码实战,让你的springboot应用识别图片中的行人、汽车、狗子、喵星人(JavaCV+YOLO4)

    欢迎访问我的GitHub https://github.com/zq2599/blog_demos 内容:所有原创文章分类汇总及配套源码,涉及Java.Docker.Kubernetes.DevOPS ...

  3. Web自动化之Headless Chrome编码实战

    API 概览 && 编码Tips 文档地址 github Chrome DevTools Protocol 协议本身的仓库 有问题可以在这里提issue github debugger ...

  4. 长篇图解etcd核心应用场景及编码实战

    大家好啊,我是字母哥,今天写一篇关于etcd的文章,其实网上也有很多关于etcd的介绍,我就简明扼要,总结提炼,期望大家通过这一篇文章掌握etcd的核心知识以及编码技能! 本文首先用大白话给大家介绍一 ...

  5. 学习笔记_Java_day14—编码实战___一个注册页面的完整流程

  6. 编码实战Web端联系人的增删改查

    首先画出分析图 实现效果如图 项目下的包如图: 实体包 package com.contactSystem.entiey; public class Contact { private String ...

  7. day14(编码实战-用户登录注册)

    day14 案例:用户注册登录 要求:3层框架,使用验证码   功能分析 注册 登录   1.1 JSP页面 regist.jsp 注册表单:用户输入注册信息: 回显错误信息:当注册失败时,显示错误信 ...

  8. 安卓topbar编码实战

    1.先在res->value下新建attrs.xml文件 <?xml version="1.0" encoding="utf-8"?> < ...

  9. Scala高手实战****第18课:Scala偏函数、异常、Lazy值编码实战及Spark源码鉴赏

    本篇文章主要讲述Scala函数式编程之偏函数,异常,及Lazy 第一部分:偏函数 偏函数:当函数有多个参数,而在使用该函数时不想提供所有参数(比如函数有3个参数),只提供0~2个参数,此时得到的函数便 ...

  10. JVM内存结构探秘及编码实战

    了解JVM内存结构的目的 在Java的开发过程中,因为有JVM自动内存管理机制,不再需要像在C.C++开发那样手动释放对象的内存空间,不容易出现内存泄漏和内存溢出的问题.但是,正是由于把内存管理的权利 ...

随机推荐

  1. Hadoop集群常用组件的命令

    1. Hadoop (1).HDFS:启动HDFS:start-dfs.sh关闭HDFS:stop-dfs.sh格式化NameNode:hdfs namenode -format查看文件系统状态:hd ...

  2. Linux-双网卡绑定bond详解

    1.什么是bond 网卡bond是通过多张物理网卡绑定为一个逻辑网卡,实现本地网卡的冗余,带宽扩容和负载均衡,在生产场景中是一种常用的技术.Kernels 2.4.12及以后的版本均供bonding模 ...

  3. 好书推荐之《深入理解JAVA虚拟机》

    大佬推进 这本书作为JVM的入门书籍,是每一个JAVA工程师必备的. 简介 这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎. 自2011年上市 ...

  4. ES6学习 第四章 字符串的新增方法

    前言 本章介绍字符串对象的新增方法.不常用的方法不做重点笔记. 本章原文链接:字符串的新增方法 includes().startsWith().endsWith() 确定一个字符串是否包含在另一个字符 ...

  5. NC15532 Happy Running

    题目链接 题目 题目描述 Happy Running, an application for runners, is very popular in CHD. In order to lose wei ...

  6. 基于 junit5 实现 junitperf 源码分析

    前言 上一节介绍了基于 junit4 实现 junitperf,但是可以发现定义变量的方式依然不够优雅. 那可以让用户使用起来更加自然一些吗? 有的,junit5 为我们带来了更加强大的功能. 拓展阅 ...

  7. STM32F407VET6烧录出现flash download failed target dll has been cancelled

    今天在通过stlink烧录一个长时间未用的STM32F407VET6 Black Board的时候, 出现错误 Internal command error Flash download failed ...

  8. 【Java复健指南02】方法的注意事项

    [方法] 方法基本内容 √访问修饰符 ​ (作用是控制方法使用的范围) ​ 可选,[有四种:public\protected\默认\private],具体在后面说 √返回类型 ​ 1.一个方法最多有一 ...

  9. SUB-LVDS 与LVDS 互联

    SUB-LVDS 与 LVDS介绍 电气规范 今天有同学问SUB-LVDS输出是否能接到LVDS输入上,以前没用过SUB-LVDS,一起学习一下. Sub-LVDS is a differential ...

  10. Golang标准库——io

    原文:Golang标准库--io 1.io io包提供了对I/O原语的基本接口.本包的基本任务是包装这些原语已有的实现(如os包里的原语),使之成为共享的公共接口,这些公共接口抽象出了泛用的函数并附加 ...