FFmpeg AAC编码实战

对音频进行编码最重要目的就是为了进行数据压缩,以此来降低数据传输和存储的成本。拿原始音频来举例,一路采样率为 44100Hz,量化位深为 16bit,声道数为 2 的声音,如果不进行编码压缩,对应的码率是:441000 Hz * 16 bit * 2 = 1411200 bps = 1.346 Mbps。一分钟的时间所需要的数据量是:1.346 Mbps * 60 s = 80.75 Mb = 10.09 MB。对于单单一路音频来说,这个数据量还是比较大的,在存储或传输时如果能进行压缩编码,可以在一定程度上提高效率。

一、FFmpeg 命令行实现 AAC 编码

1.1、基本使用:

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

-c:a 设置音频编码器,c表示codec(编解码器),a表示audio(音频)。等价写法 -codec:a 或者 -acodec。需要注意的是这是输出参数。

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

1.2、libfdk_aac 常用参数:

-b:a 设置输出比特率(比如 -b:a 96k,开启 VBR 模式会忽略此选项)

$ ffmpeg -i in.wav -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)

为了适用于不同的应用场合,在 MPEG-2 AAC 标准中定义了三种不同的编码规格:
1、MPEG-2 AAC LC(Low Complexity),低复杂度规格。用于要求在有限的存储空间和计算能力的条件下进行压缩场合。在这种框架中,没有预测和增益控制这两种工具,TNS 的阶数比较低。编码码率在 96kbps-192kbps 之间的可以用该规格。MP4 的音频部分常用该规格。
2、MPEG-2 AAC Main,主规格。具有最高的复杂度,可以用于存储量和计算能力都很充足的场合。在这种框架中,利用了除增益控制以外的所有编码工具来提高压缩效率。
3、MPEG-2 AAC SSR(Scalable Sample Rate),可变采样率规格。在这种框架中,使用了增益控制工具,但是预测和耦合工具是不被允许的,具有较低的带宽和 TNS 阶数。对于最低的一个 PQF 子带不使用增益控制工具。当带宽降低时,SSR 框架的复杂度也可降低,特别适应于网络带宽变化的场合。
在 MPEG-4 AAC 标准中除了继承上面的三种规格进行改进外,还新增了三种编码规格:
1、MPEG-4 AAC LC(Low Complexity),低复杂度规格。
2、MPEG-4 AAC Main,主规格。
3、MPEG-4 AAC SSR(Scalable Sample Rate),可变采样率规格。
4、MPEG-4 AAC LD(Low Delay),低延迟规格。AAC 是感知型音频编解码器,可以在较低的比特率下提供很高质量的主观音质。但是这样的编解码器在低比特率下的算法延时往往超过 100ms,所以并不适合实时的双向通信。结合了感知音频编码和双向通信必须的低延时要求。可以保证最大 20ms 的算法延时和包括语音和音乐的信号的很好的音质。
5、MPEG-4 AAC LTP(Long Term Predicition),长时预测规格。在 Main Profile 的基础上增加了前向预测。
6、MPEG-4 AAC HE(High Efficiency),高效率规格。混合了 AAC 与 SBR(Spectral Band Replication,频段复制)技术。而新版本的 HE,即 HE v2 是 AAC 加上 SBR 和 PS(Parametric Stereo,参数立体声)技术。这样能在同样的效果上使用更低的码率。在编码码率为 32-96 Kbps 之间的音频文件时,建议首选这种规格。

设置了音频编码规格,会自动设置一个合适的输出比特率,也可以通过 -b:a 自行设置。

$ ffmpeg -i in.wav -profile:a aac_low out.aac

设置 aac_low 以外的参数报错:

[adts @ 0x7fe95e83d000] MPEG-4 AOT 21 is not allowed in ADTS
Could not write header for output file #0 (incorrect codec parameters ?): Invalid data found when processing input
Error initializing output stream 0:0 --

-vbr 设置可变比特率模式

取值选项(取值选项与对应的比特率大致范围,当前仅 aac_low 支持 VBR 模式):

0: 默认值,关闭 VBR,开启 CBR

1: 32 kbps/channel,质量最低,音质仍很好
2: 40 kbps/channel
3: 48-56 kbps/channel
4: 64 kbps/channel
5: about 80-96 kbps/channel,质量最高

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

AAC 编码文件扩展名主要有 3 种:aac、m4a、mp3。

二、FFmpeg 编程实现 AAC 编码

AAC 编码流程:

1、AAC 编码需要用到 2 个库:

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

2、定义变量:

// 返回值
int ret;

// 编码器
AVCodec *codec = nullptr;
// 上下文
AVCodecContext *ctx = nullptr;
// 用来存放编码前的数据
AVFrame *frame;
// 用来存放编码后的数据
AVPacket *pkt;

QFile inFile(in.filename);
QFile outFile(outFilename);

3、查找音频编码器:

我们使用的是官方推荐的高效的 AAC 编码器 libfdk_aac:

codec = avcodec_find_encoder_by_name("libfdk_aac");
if (!codec) {
    qDebug() << "encoder libfdk_aac not found";
    return;
}

4、检查编码器是否支持编码格式:

if (!check_sample_fmt(codec, in.sampleFmt)) {
    qDebug() << "Encoder dose not support sample format: " << av_get_sample_fmt_name(in.sampleFmt);
    goto end;
}
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;
}

此处 AV_SAMPLE_FMT_NONE 并不是编码器支持的格式,是 FFmpeg 开发人员设计的一个标记,用来防止我们遍历编码器支持的采样格式数组越界。当前我们使用的 AAC 编码器 libfdk_aac 是非官方的,如果想使用 FFmpeg 官方的编码器,可以使用以下方式获取 FFmpeg 官方默认的 AAC 编码器:

AVCodec *codec1 = avcodec_find_decoder(AV_CODEC_ID_AAC);
AVCodec *codec2 = avcodec_find_decoder_by_name("aac”);
qDebug() << "codec1->name:" <<  codec1->name;
qDebug() << "isTheSameCodec: " << (codec1 == codec2);

打印:

codec1->name: aac
isTheSameCodec:  true

说明我们使用 ID 和使用名称获取的官方编码器是同一个编码器。如果我们改成官方编码器运行代码,控制台会输出:

unsupported sample format s16

是因为不同的编码器对输入数据的格式要求是不一样的。s16 是 libfdk_aac 支持的采样格式。我们可以遍历打印 codec->sample_fmts 可以知道官方 AAC 编码器支持的格式是 fltp,所以如果想使用 FFmpeg 官方的 AAC 编码器的话,需要把 pcm 重采样成 fltp 格式。

5、设置编码上下文参数:

// 音频采样格式
ctx->sample_fmt = in.sampleFmt;
// 音频采样率
ctx->sample_rate = in.sampleRate;
// 声道布局
ctx->channel_layout = in.chLayout;
// 音频编码规格
ctx->profile = FF_PROFILE_AAC_HE_V2;
// 比特率
ctx->bit_rate = 32000;

6、打开编码器:

ret = avcodec_open2(ctx, codec, nullptr);
if (ret < 0) {
    ERRBUF(ret);
    qDebug() << "could not open codec: " << errbuf;
    goto end;
}

7、创建 packet:

pkt = av_packet_alloc();
if (!pkt) {
    qDebug() << "could not allocate audio packet";
    goto end;
}

8、创建 frame 并创建 frame 的数据缓冲区:

frame = av_frame_alloc();
if (!frame) {
    qDebug() << "could not allocate audio frame";
    goto end;
}

// frame缓冲区中的样本帧数量(由ctx->frame_size决定)
frame->nb_samples = ctx->frame_size;
// 音频采样格式
frame->format = ctx->sample_fmt;
// 声道布局
frame->channel_layout = ctx->channel_layout;

// 利用nb_samples、format、channel_layout创建frame的数据缓冲区
ret = av_frame_get_buffer(frame, 0);
if (ret < 0) {
    ERRBUF(ret);
    qDebug() << "could not allocate audio data buffers: " << errbuf;
    goto end;
}

9、打开文件:

if (!inFile.open(QFile::ReadOnly)) {
    qDebug() << "open file failure: " << in.filename;
    goto end;
}

if (!outFile.open(QFile::WriteOnly)) {
    qDebug() << "open file failure: " << outFilename;
    goto end;
}

10、编码:

// 读取数据到frame中
while ((ret = inFile.read((char *)frame->data[0], frame->linesize[0])) > 0) {
    if (encode(ctx, frame, pkt, outFile) < 0) {
        goto end;
    }
}

// 刷出输出缓冲区中剩余数据
encode(ctx, nullptr, pkt, outFile);
static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, QFile &outFile)
{
    // 发送数据到音频编码器
    int ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "error sending the frame to the codec: " << errbuf;
        return ret;
    }

    while (true) {
        // 从编码器中获取编码后的数据
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            ERRBUF(ret);
            qDebug() << "error encode audio frame: " << errbuf;
            return ret;
        }

        outFile.write((const char *)pkt->data, pkt->size);
        av_packet_unref(pkt);
    }
    return 0;
}

11、关闭文件,释放资源:

end:
    inFile.close();
    outFile.close();

    av_packet_free(&pkt);
    av_frame_free(&frame);
    avcodec_free_context(&ctx);

11、最后调用我们写好的方法:

    AudioEncodeSpec spec;
    spec.filename = "/users/mac/Downloads/music/ar44100ac2s16le.pcm";
    spec.sampleRate = 44100;
    spec.sampleFmt = AV_SAMPLE_FMT_S16;
    spec.chLayout = AV_CH_LAYOUT_STEREO;
    FFmpegUtils::aacEncode(spec, "/users/mac/Downloads/music/aac/ar44100ac2s16le-02.aac");

到此我们 AAC 编码就搞定了,使用ffplay播放刚刚代码生成的aac文件,歌声听起来是没有问题的,但是听到正确的歌声不一定就代表我们的代码是没有问题的,我们要使用同样的参数,利用 FFmpeg 命令行工具生成 aac 文件,对比一下使用命令行工具生成的 aac 文件的大小是否和我们刚刚使用代码生成的 aac 文件的大小是否完全一样(要比较精确到字节的大小)。

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

比较两个文件的大小:

$ ls -al
# ffmpeg命令行工具生成的aac文件
-rw-r--r--   1 mac  staff  1203351 April 5 19:57 ar44100ac2s16le-01.aac
# 代码生成的aac文件
-rw-r--r--@  1 mac  staff  1203371 April 5 19:58 ar44100ac2s16le-02.aac

我们发现代码代码生成的 aac 文件比 FFmpeg 命令行工具生成的文件要大20字节,问题出在了什么地方呢?是因为读取 pcm 数据到 frame 中的时候,最后一次读取的数据并不足以填充 frame 缓冲区,frame->linesize[0] 是我们期望读取到frame缓冲区中的数据大小,但实际上最后一次读取 pcm 数据到frame缓冲区的数据的实际大小ret要小于 frame->linesize[0] 的大小,所以我们需要判断一下读取的数据是否不足以填充frame缓冲区:

// 读取数据到frame中
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 / (bytes * chs);
    }
//----------------------------------------------------------------------------
    if (encode(ctx, frame, pkt, outFile) < 0) {
        goto end;
    }
}

// 刷出输出缓冲区中剩余数据
encode(ctx, nullptr, pkt, outFile);

那么如何知道需要修改的是 frame->nb_samples 呢?需要查看源码,通过源码我们会发现
avcodec_send_frame(发送数据到音频编码器)时会通过 frame->nb_samples 参数判断要发送到编码器数据的大小。

参考 FFmpeg 源码 example: /ffmpeg-4.3.2/doc/examples/encode_audio.c

推荐阅读更多精彩内容