07-?音视频技术核心知识|AAC编码【AAC编码器解码器、编译FFmpeg、AAC编码实战、AAC解码实战】

一、前言

顺应时代的技术发展潮流,逐步学习并掌握音视频技术核心知识,让技术落地,让知识赋能生活,让科技造福千万灯火。

二、AAC编码

AAC(Advanced Audio Coding,译为:高级音频编码),是由Fraunhofer IIS、杜比实验室、AT&T、Sony、Nokia等公司共同开发的有损音频编码和文件格式。

1. 对比MP3

AAC被设计为MP3格式的后继产品,通常在相同的比特率下可以获得比MP3更高的声音质量,是iPhone、iPod、iPad、iTunes的标准音频格式。

AAC相较于MP3的改进包含:

  • 更多的采样率选择:8kHz ~ 96kHz,MP3为16kHz ~ 48kHz
  • 更高的声道数上限:48个,MP3在MPEG-1模式下为最多双声道,MPEG-2模式下5.1声道
  • 改进的压缩功能:以较小的文件大小提供更高的质量
  • 改进的解码效率:需要较少的处理能力进行解码
  • ……

2. 规格

AAC是一个庞大家族,为了适应不同场合的需要,它有很多种规格可供选择。下面列举其中的9种规格(Profile):

  • MPEG-2 AAC LC:低复杂度规格(Low Complexity)
  • MPEG-2 AAC Main:主规格
  • MPEG-2 AAC SSR:可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LC:低复杂度规格(Low Complexity)
    • 现在的手机比较常见的MP4文件中的音频部分使用了该规格
  • MPEG-4 AAC Main:主规格
  • MPEG-4 AAC SSR:可变采样率规格(Scaleable Sample Rate)
  • MPEG-4 AAC LTP:长时期预测规格(Long Term Predicition)
  • MPEG-4 AAC LD:低延迟规格(Low Delay)
  • MPEG-4 AAC HE:高效率规格(High Efficiency)

最早是基于MPEG-2标准,称为:MPEG-2 AAC。后来MPEG-4标准在原来基础上增加了一些新技术,称为:MPEG-4 AAC。

3. LC和HE

虽然上面列举了9种规格,但我们目前只需要把注意力放在常用的LC和HE上。下图很好的展示了从LC到HE的发展历程。

从LC到HE

从LC到HE

3.1 LC

LC适合中等比特率,比如96kbps ~ 192kbps之间。

MPEG-4 AAC LC等价于:

  • MPEG-2 AAC LC + PNS

PNS(Perceptual Noise Substitution)译为:感知噪声替代。

  • PNS可以提高AAC的编码效率

3.2 HE

HE有v1和v2两个版本,适合低比特率:

  • v1:适合48kbps ~ 64kbps
  • v2:适合低于32kbps,可在低至32kbps的比特率下提供接近CD品质的声音

3.2.1 v1

MPEG-4 AAC HE v1的别名:

  • aacPlus v1
  • eAAC
  • AAC+
  • CT-aacPlus(Coding Technologies)
    • Coding Technologies是瑞典是一家技术公司,率先在AAC中使用了SBR技术
    • 在2007年,被杜比实验室(Dolby Laboratories)以2.5亿美元收购

MPEG-4 AAC HE v1等价于:

  • MPEG-4 AAC LC + SBR

SBR

SBR(Spectral Band Replication)译为:频段复制。

  • 是一种增强的压缩技术
  • 可以将高频信号存储在少量的SBR data中
  • 解码器可以根据SBR data恢复出高频信号

3.2.2 v2

MPEG-4 AAC HE v2的别名:

  • aacPlus v2
  • AAC++
  • eAAC+、Enhanced AAC+

MPEG-4 AAC HE v2等价于:

  • MPEG-4 AAC HE v1 + PS

PS

PS(Parametric Stereo)译为:参数立体声。

  • 是一种有损的音频压缩算法,可以进一步提高压缩率
  • 可以将左右声道信号组合成单声道信号,声道之间的差异信息存储到少量的PS data中(大概占2 ~ 3kbps)
  • 解码器可以根据PS data中恢复出立体声信号

4. 编解码器

如果想对PCM数据进行AAC编码压缩,那么就要用到AAC码器(encoder)。
如果想将AAC编码后的数据解压出PCM数据,那么就要用到AAC码器(decoder)。

这里只列举几款常用的AAC编解码器:

  • Nero AAC

    • 支持LC/HE规格
    • 目前已经停止开发维护
  • FFmpeg AAC

    • 支持LC规格
    • FFmpeg官方内置的AAC编解码器,在libavcodec库中
      • 编解码器名字叫做aac
      • 在开发过程中通过这个名字找到编解码器
  • FAAC(Freeware Advanced Audio Coder)

    • 支持LC规格
    • 可以集成到FFmpeg的libavcodec中
      • 编解码器名字叫做libfaac
      • 在开发过程中通过这个名字找到编解码器,最后调用FAAC库的功能
    • 从2016年开始,FFmpeg已经移除了对FAAC的支持
  • Fraunhofer FDK AAC

    • 支持LC/HE规格
    • 目前质量最高的AAC编解码器
    • 可以集成到FFmpeg的libavcodec中
      • 编解码器名字叫做libfdk_aac
      • 在开发过程中通过这个名字找到编解码器,最后调用FDK AAC库的功能

编码质量排名:Fraunhofer FDK AAC > FFmpeg AAC > FAAC。

5. FDK AAC

在网上下载的编译版FFmpeg,通常都是没有集成libfdk_aac的。可以通过命令行查看FFmpeg目前集成的AAC编解码器。

# windows
ffmpeg -codecs | findstr aac






# mac
ffmpeg -codecs | grep aac

我这边的输出结果是:

DEAIL. aac                  AAC (Advanced Audio Coding) (decoders: aac aac_fixed )
D.AIL. aac_latm             AAC LATM (Advanced Audio Coding LATM syntax)

很显然,并没有包含libfdk_aac。

这里给出1个比较推荐的方案:自己手动编译FFmpeg源码,将libfdk_aac集成到FFmpeg中。

  • 自己手动编译的话,想集成啥就集成啥
  • 可以把你想要的东西都塞到FFmpeg中,不想要的就删掉
  • 也就是根据自己的需要对FFmpeg进行裁剪

三、编译FFmpeg

本文来详细讲解一下:如何在Mac、Windows环境下成功编译FFmpeg。

1. 目标

这里先提前说明一下,最后希望达到的效果:

  • 编译出ffmpegffprobeffplay三个命令行工具
  • 只产生动态库,不产生静态库
  • fdk-aacx264x265集成到FFmpeg中
    • x264、x265会在以后讲解的视频模块中用到

2. 下载源码

下载源码ffmpeg-4.3.2.tar.xz,然后解压。

FFmpeg源码结构

3. Mac编译

3.1 依赖项

  • brew install yasm
    • ffmpeg的编译过程依赖yasm
    • 若未安装yasm会出现错误:nasm/yasm not found or too old. Use –disable-x86asm for a crippled build.
  • brew install sdl2
    • ffplay依赖于sdl2
    • 如果缺少sdl2,就无法编译出ffplay
  • brew install fdk-aac
    • 不然会出现错误:ERROR: libfdk_aac not found
  • brew install x264
    • 不然会出现错误:ERROR: libx264 not found
  • brew install x265
    • 不然会出现错误:ERROR: libx265 not found

其实x264x265sdl2都在曾经执行brew install ffmpeg的时候安装过了。

  • 可以通过brew list的结果查看是否安装过
    • brew list | grep fdk
    • brew list | grep x26
    • brew list | grep -E ‘fdk|x26’
  • 如果已经安装过,可以不用再执行brew install

3.2 configure

首先进入源码目录。

# 我的源码放在了Downloads目录下
cd ~/Downloads/ffmpeg-4.3.2

然后执行源码目录下的configure脚本,设置一些编译参数,做一些编译前的准备工作。

./configure --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl  --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265

  • –prefix

    • 用以指定编译好的FFmpeg安装到哪个目录
    • 一般放到**/usr/local/ffmpeg**中即可
  • –enable-shared

    • 生成动态库
  • –disable-static

    • 不生成静态库
  • –enable-libfdk-aac

    • 将fdk-aac内置到FFmpeg中
  • –enable-libx264

    • 将x264内置到FFmpeg中
  • –enable-libx265

    • 将x265内置到FFmpeg中
  • –enable-gpl

  • –enable-nonfree

你可以通过configure –help命令查看每一个配置项的作用。

./configure --help | grep static




# 结果如下所示

--disable-static         do not build static libraries [no]

3.3 编译

接下来开始解析源代码目录中的Makefile文件,进行编译。-j8表示允许同时执行8个编译任务。

make -j8

对于经常在类Unix系统下接触C/C++开发的小伙伴来说,Makefile必然是不陌生的。这里给不了解Makefile的小伙伴简单科普一下:

  • Makefile描述了整个项目的编译和链接等规则
    • 比如哪些文件需要编译?哪些文件不需要编译?哪些文件需要先编译?哪些文件需要后编译?等等
  • Makefile可以使项目的编译变得自动化,不需要每次都手动输入一堆源文件和参数
    • 比如原来需要这么写:gcc test1.c test2.c test3.c -o test

3.4 安装

将编译好的库安装到指定的位置:/usr/local/ffmpeg

make install

安装完毕后,/usr/local/ffmpeg的目录结构如下所示。

FFmpeg目录结构

3.5 配置PATH

为了让bin目录中的ffmpegffprobeffplay在任意位置都能够使用,需要先将bin目录配置到环境变量PATH中。

# 编辑.zprofile
vim ~/.zprofile






# .zprofile文件中写入以下内容
export PATH=/usr/local/ffmpeg/bin:$PATH


# 让.zprofile生效
source ~/.zprofile

如果你用的是bash,而不是zsh,只需要将上面的**.zprofile换成.bash_profile**。

3.6 验证

接下来,在命令行上进行验证。

ffmpeg -version




# 结果如下所示

ffmpeg version 4.3.2 Copyright (c) 2000-2021 the FFmpeg developers
built with Apple clang version 12.0.0 (clang-1200.0.32.29)
configuration: --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265
libavutil      56. 51.100 / 56. 51.100
libavcodec     58. 91.100 / 58. 91.100
libavformat    58. 45.100 / 58. 45.100
libavdevice    58. 10.100 / 58. 10.100
libavfilter     7. 85.100 /  7. 85.100
libswscale      5.  7.100 /  5.  7.100
libswresample   3.  7.100 /  3.  7.100
libpostproc    55.  7.100 / 55.  7.100

此时,你完全可以通过brew uninstall ffmpeg卸载以前安装的FFmpeg。

4. Windows编译

configureMakefile这一套工具是用在类Unix系统上的(Linux、Mac等),默认无法直接用在Windows上。

这里介绍其中一种可行的解决方案

  • 使用MSYS2软件在Windows上模拟出Linux环境
  • 结合使用MinGW对FFmpeg进行编译

4.1 下载安装MSYS2

进入MSYS2官网下载安装包(我这边下载的是:msys2-x86_64-20210228.exe),然后进行安装。

安装完毕后打开命令行工具mingw64.exe

mingw64.exe

4.2 安装依赖

pacman(Package Manager)是一个包管理工具。

  • pacman -Sl:搜索有哪些包可以安装
  • pacman -S:安装
  • pacman -R:卸载
# 查看是否有fdk、SDL2相关包(E表示跟一个正则表达式,i表示不区分大小写)
pacman -Sl | grep -Ei 'fdk|sdl2'






# 结果如下所示
mingw32 mingw-w64-i686-SDL2 2.0.14-2
mingw32 mingw-w64-i686-SDL2_gfx 1.0.4-1
mingw32 mingw-w64-i686-SDL2_image 2.0.5-1
mingw32 mingw-w64-i686-SDL2_mixer 2.0.4-2
mingw32 mingw-w64-i686-SDL2_net 2.0.1-1
mingw32 mingw-w64-i686-SDL2_ttf 2.0.15-1
mingw32 mingw-w64-i686-fdk-aac 2.0.1-1
mingw64 mingw-w64-x86_64-SDL2 2.0.14-2
mingw64 mingw-w64-x86_64-SDL2_gfx 1.0.4-1
mingw64 mingw-w64-x86_64-SDL2_image 2.0.5-1
mingw64 mingw-w64-x86_64-SDL2_mixer 2.0.4-2
mingw64 mingw-w64-x86_64-SDL2_net 2.0.1-1
mingw64 mingw-w64-x86_64-SDL2_ttf 2.0.15-1
mingw64 mingw-w64-x86_64-fdk-aac 2.0.1-1

接下来,安装各种依赖包。

# 编译工具链
pacman -S mingw-w64-x86_64-toolchain






pacman -S mingw-w64-x86_64-yasm






pacman -S mingw-w64-x86_64-SDL2




pacman -S mingw-w64-x86_64-fdk-aac

pacman -S mingw-w64-x86_64-x264

pacman -S mingw-w64-x86_64-x265



# 需要单独安装make
pacman -S make

关于软件包相关的默认路径:

  • 下载目录:%MSYS2_HOME%/var/cache/pacman/pkg
  • 安装目录:%MSYS2_HOME%/mingw64
  • **%MSYS2_HOME%**是指MSYS2的安装目录

4.3 configure

我的源码是放在F:/Dev/ffmpeg-4.3.1,输入cd /f/dev/ffmpeg-4.3.1即可进入源码目录。然后执行configure

./configure --prefix=/usr/local/ffmpeg --enable-shared --disable-static --enable-gpl  --enable-nonfree --enable-libfdk-aac --enable-libx264 --enable-libx265

4.4 编译、安装

make -j8 && make install

FFmpeg最终会被安装到**%MSYS2_HOME%/usr/local/ffmpeg**目录中。

FFmpeg的安装目录

4.5 bin

此时bin目录中的ffmpeg、ffprobe、ffplay还是没法使用的,因为缺少相关的dll,需要从**%MSYS2_HOME%/mingw64/bin中拷贝,或者将%MSYS2_HOME%/mingw64/bin**配置到环境变量Path中。

需要拷贝的dll有:libwinpthread-1SDL2zlib1.dllliblzma-5libbz2-1libiconv-2libgcc_s_seh-1libstdc++-6libx265libx264-159libfdk-aac-2

FFmpeg的bin目录

4.6 Path

最后建议将**%FFMPEG_HOME%/bin**目录配置到环境变量Path中。

在命令行输入ffmpeg -version,一切大功告成!

查看版本

四、AAC编码实战

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

1. 要求

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!

1.1 采样格式

必须是16位整数PCM。

1.2 采样率

支持的采样率有(Hz):

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

2. 命令行

2.1 基本使用

最简单的用法如下所示:

# 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

2.2 常用参数

  • -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

2.3 文件格式

我曾在《重识音频》中提到,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

3. 编程

需要用到2个库:

extern "C" {


#include <libavcodec/avcodec.h>


#include <libavutil/avutil.h>

}







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

3.1 函数声明

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

extern "C" {


#include <libavcodec/avcodec.h>


}


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



class FFmpegs {
public:
    FFmpegs();

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

3.2 函数实现

3.2.1 变量定义

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






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

// 返回结果
int ret = 0;



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

3.2.2 获取编码器

下面的代码可以获取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;




}









3.2.3 检查采样格式

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

// 检查采样格式
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;
}

3.2.4 创建上下文

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;

3.2.5 打开编码器

// 打开编码器
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);

3.2.6 创建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;
}

3.2.7 创建AVPacket

// 创建AVPacket

pkt = av_packet_alloc();

if (!pkt) {

    qDebug() << "av_packet_alloc error";

    goto end;




}









3.2.8 打开文件

// 打开文件

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;

}

3.2.9 开始编码

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

3.2.10 资源回收

end:

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






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

3.3 函数调用

AudioEncodeSpec in;
in.filename = "F:/in.pcm";
in.sampleRate = 44100;
in.sampleFmt = AV_SAMPLE_FMT_S16;
in.chLayout = AV_CH_LAYOUT_STEREO;


FFmpegs::aacEncode(in, "F:/out.aac");

五、AAC解码实战

本文主要讲解:如何将AAC编码后的数据解码成PCM。

1. 命令行

用法非常简单:

ffmpeg -c:a libfdk_aac -i in.aac -f s16le out.pcm
  • -c:a libfdk_aac

    • 使用fdk-aac解码器
    • 需要注意的是:这个参数要写在aac文件那边,也就是属于输入参数
  • -f s16le

    • 设置PCM文件最终的采样格式

2. 编程

需要用到2个库:

extern "C" {


#include <libavcodec/avcodec.h>


#include <libavutil/avutil.h>

}







#define ERROR_BUF(ret) \
    char errbuf[1024]; \
    av_strerror(ret, errbuf, sizeof (errbuf));

2.1 函数声明

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

// 解码后的PCM参数
typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioDecodeSpec;


class FFmpegs {
public:
    FFmpegs();



    static void aacDecode(const char *inFilename,
                          AudioDecodeSpec &out);
};

2.2 函数实现

2.2.1 变量定义

// 输入缓冲区的大小
#define IN_DATA_SIZE 20480
// 需要再次读取输入文件数据的阈值
#define REFILL_THRESH 4096






// 返回结果
int ret = 0;


// 每次从输入文件中读取的长度
int inLen = 0;
// 是否已经读取到了输入文件的尾部
int inEnd = 0;



// 用来存放读取的文件数据
// 加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界
char inDataArray[IN_DATA_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = inDataArray;


// 文件
QFile inFile(inFilename);
QFile outFile(out.filename);

// 解码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
// 解析器上下文
AVCodecParserContext *parserCtx = nullptr;


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

2.2.2 获取解码器

// 获取解码器
codec = avcodec_find_decoder_by_name("libfdk_aac");
if (!codec) {

    qDebug() << "decoder libfdk_aac not found";
    return;




}









2.2.3 初始化解析器上下文

// 初始化解析器上下文
parserCtx = av_parser_init(codec->id);
if (!parserCtx) {
    qDebug() << "av_parser_init error";
    return;




}









2.2.4 创建上下文

// 创建上下文

ctx = avcodec_alloc_context3(codec);

if (!ctx) {

    qDebug() << "avcodec_alloc_context3 error";

    goto end;




}









2.2.5 创建AVPacket

// 创建AVPacket

pkt = av_packet_alloc();

if (!pkt) {

    qDebug() << "av_packet_alloc error";

    goto end;




}









2.2.6 创建AVFrame

// 创建AVFrame

frame = av_frame_alloc();

if (!frame) {

    qDebug() << "av_frame_alloc error";

    goto end;




}









2.2.7 打开解码器

// 打开解码器
ret = avcodec_open2(ctx, codec, nullptr);

if (ret < 0) {

    ERROR_BUF(ret);

    qDebug() << "avcodec_open2 error" << errbuf;

    goto end;

}

2.2.8 打开文件

// 打开文件

if (!inFile.open(QFile::ReadOnly)) {

    qDebug() << "file open error:" << inFilename;
    goto end;

}

if (!outFile.open(QFile::WriteOnly)) {

    qDebug() << "file open error:" << out.filename;
    goto end;

}

2.2.9 解码

// 读取数据
inLen = inFile.read(inData, IN_DATA_SIZE);
while (inLen > 0) {
    // 经过解析器上下文处理
    ret = av_parser_parse2(parserCtx, ctx,
                           &pkt->data, &pkt->size,
                           (uint8_t *) inData, inLen,
                           AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "av_parser_parse2 error" << errbuf;
        goto end;
    }

	// 跳过已经解析过的数据
    inData += ret;
    // 减去已经解析过的数据大小
    inLen -= ret;


    // 解码
    if (pkt->size > 0 && decode(ctx, pkt, frame, outFile) < 0) {
        goto end;
    }

    // 如果数据不够了,再次读取文件
    if (inLen < REFILL_THRESH && !inEnd) {
        // 剩余数据移动到缓冲区前
        memmove(inDataArray, inData, inLen);
        inData = inDataArray;


        // 跨过已有数据,读取文件数据
        int len = inFile.read(inData + inLen, IN_DATA_SIZE - inLen);
        if (len > 0) {
            inLen += len;
        } else {
            inEnd = 1;
        }
    }
}

// flush解码器
//    pkt->data = NULL;
//    pkt->size = 0;
decode(ctx, nullptr, frame, outFile);

具体的解码操作在decode函数中。

static int decode(AVCodecContext *ctx,
                  AVPacket *pkt,
                  AVFrame *frame,
                  QFile &outFile) {
    // 发送压缩数据到解码器
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_packet error" << errbuf;
        return ret;
    }



    while (true) {
        // 获取解码后的数据
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERROR_BUF(ret);
            qDebug() << "avcodec_receive_frame error" << errbuf;
            return ret;
        }
        // 将解码后的数据写入文件
        outFile.write((char *) frame->data[0], frame->linesize[0]);
    }
}

2.2.10 设置输出参数

// 设置输出参数
out.sampleRate = ctx->sample_rate;
out.sampleFmt = ctx->sample_fmt;
out.chLayout = ctx->channel_layout;

2.2.11 释放资源

end:

    inFile.close();
    outFile.close();
    av_frame_free(&frame);
    av_packet_free(&pkt);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);

2.3 函数调用

AudioDecodeSpec out;
out.filename = "F:/out.pcm";
FFmpegs::aacDecode("F:/in.aac", out);
// 44100
qDebug() << out.sampleRate;
// s16
qDebug() << av_get_sample_fmt_name(out.sampleFmt);
// 2
qDebug() << av_get_channel_layout_nb_channels(out.chLayout);

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYjGnGyI' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片