七、通过libfdk_aac编解码器实现aac音频和pcm的编解码

前言

测试环境:

  • ffmpeg的4.3.2自行编译版本
  • windows环境
  • qt5.12

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

AAC相较于MP3的改进包含:

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

AAC编码为了使用不同场景的需求,设计了很多规格

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

众多规格中只需关注LC和HE

pcm与aac的转换需要AAC编解码器(如下列举几种常用的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。

由于libfdk_aac最好,但是网上下载好的ffmpeg编译好的版本不带libfdk_aac编解码器。所以我们只能自行编译ffmpeg。

如下命令可以查看FFmpeg目前集成的AAC编解码器

c 复制代码
ffmpeg -codecs | findstr aac

自己手动编译FFmpeg源码,将libfdk_aac集成到FFmpeg中,这种方式最好,但在windows环境下较为麻烦。

因为编译源码需要在类Unix系统上的(Linux、Mac等),默认无法直接用在Windows上。所以必须先用MSYS2软件在Windows上模拟出Linux环境,然后再在其中用MinGW软件对FFmpeg进行编译。

链接:windows下msys2编译64位的ffmpeg源码

编译好源码后,需要把.pro文件配置成新编译的源码。

fdk-aac对需要编解码的pcm音频有一定的格式要求

  • 采样格式必须为16位整数PCM
  • 采样率只支持:8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、88200、96000

命令行将pcm和wav文件编码成aac音频

c 复制代码
# pcm -> aac
ffmpeg -ar 44100 -ac 2 -f s16le -i in.pcm -c:a libfdk_aac out.aac
-ar 44100 -ac 2 -f s16le   --PCM输入数据的参数
-c:a	 设置音频编码器,c表示codec(编解码器),a表示audio(音频)。 等价写法 -codec:a或-acodec
    
# wav -> aac
ffmpeg -i in.wav -c:a libfdk_aac out.aac   

默认生成的aac文件是LC规格的。aac文件比之前的pcm文件小了很多很多。

aac的缩写还可以是m4a和mp4。虽然现在都只认为mp4是视频文件

首先是pcm编码为aac

完整代码

AacEncodeThread.h

c 复制代码
#ifndef AACENCODETHREAD_H
#define AACENCODETHREAD_H

#include <QFile>
#include <QObject>
#include <QThread>

extern "C" {
#include <libavformat/avformat.h>
}

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

class AacEncodeThread : public QThread
{
    Q_OBJECT
public:
    explicit AacEncodeThread(QObject *parent = nullptr);
    ~AacEncodeThread();

    static int check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt);
    static int encode(AVCodecContext *ctx,AVFrame *frame,AVPacket *pkt,QFile &outFile);
    static void aacEncode(AudioEncodeSpec &in,const char *outFilename);

signals:


    // QThread interface
protected:
    virtual void run() override;
};

#endif // AACENCODETHREAD_H

AacEncodeThread.cpp

c 复制代码
#include "aacencodethread.h"

#include <QDebug>
#include <QFile>

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

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

AacEncodeThread::AacEncodeThread(QObject *parent) : QThread(parent)
{
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacEncodeThread::finished,
            this, &AacEncodeThread::deleteLater);
}

AacEncodeThread::~AacEncodeThread()
{
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";
}

// 检查采样格式
int AacEncodeThread::check_sample_fmt(const AVCodec *codec,enum AVSampleFormat sample_fmt) {
    const enum AVSampleFormat *p = codec->sample_fmts;

    while (*p != AV_SAMPLE_FMT_NONE) {
//        qDebug() << av_get_sample_fmt_name(*p);
        if (*p == sample_fmt) return 1;
        p++;
    }
    return 0;
}

// 音频编码
// 返回负数:中途出现了错误
// 返回0:编码操作正常完成
int AacEncodeThread::AacEncodeThread::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 (ret >= 0)
    while (true) {
        ret = avcodec_receive_packet(ctx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            // 继续读取数据到frame,然后送到编码器
            return 0;
        } else if (ret < 0) { // 其他错误
            return ret;
        }

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

        // 释放pkt内部的资源
        av_packet_unref(pkt);
    }
}

void AacEncodeThread::aacEncode(AudioEncodeSpec &in, const char *outFilename)
{
    // 文件
    QFile inFile(in.filename);
    QFile outFile(outFilename);

    // 返回结果
    int ret = 0;

    // 编码器
    AVCodec *codec = nullptr;

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

    // 存放编码前的数据(pcm)
    AVFrame *frame = nullptr;

    // 存放编码后的数据(aac)
    AVPacket *pkt = nullptr;

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

    // libfdk_aac对输入数据的要求:采样格式必须是16位整数
    // 检查输入数据的采样格式
    if (!check_sample_fmt(codec, in.sampleFmt)) {
        qDebug() << "unsupported sample format"
                 << av_get_sample_fmt_name(in.sampleFmt);
        return;
    }

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

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

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

    // 创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        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创建缓冲区
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "av_frame_get_buffer error" << errbuf;
        goto end;
    }

    // 创建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中
    while ((ret = inFile.read((char *) frame->data[0],
                              frame->linesize[0])) > 0) {
        // 从文件中读取的数据,不足以填满frame缓冲区
        if (ret < frame->linesize[0]) {
            int bytes = av_get_bytes_per_sample((AVSampleFormat) frame->format);
            int ch = av_get_channel_layout_nb_channels(frame->channel_layout);
            // 设置真正有效的样本帧数量
            // 防止编码器编码了一些冗余数据
            frame->nb_samples = ret / (bytes * ch);
        }

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

    // 刷新缓冲区
    encode(ctx, nullptr, pkt, outFile);

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

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

    qDebug() << "线程正常结束";
}

void AacEncodeThread::run()
{
    AudioEncodeSpec in;
    in.filename = "E:/media/test.pcm";
    in.sampleRate = 44100;
    in.sampleFmt = AV_SAMPLE_FMT_S16;
    in.chLayout = AV_CH_LAYOUT_STEREO;

    aacEncode(in, "E:/media/test.aac");
}

线程调用:

c 复制代码
void MainWindow::on_pushButton_aac_encode_clicked()
{
    m_pAacEncodeThread=new AacEncodeThread(this);
    m_pAacEncodeThread->start();
}

注意:.h文件中提前声明了以下全局变量

c 复制代码
AacEncodeThread *m_pAacEncodeThread=nullptr;

下面是aac解码成pcm

完整代码

AacDecodeThread.h

c 复制代码
#ifndef AACDECODETHREAD_H
#define AACDECODETHREAD_H

#include <QFile>
#include <QObject>
#include <QThread>

extern "C" {
#include <libavformat/avformat.h>
}

typedef struct {
    const char *filename;
    int sampleRate;
    AVSampleFormat sampleFmt;
    int chLayout;
} AudioDecodeSpec;

class AacDecodeThread : public QThread
{
    Q_OBJECT
public:
    explicit AacDecodeThread(QObject *parent = nullptr);
    ~AacDecodeThread();

    static int decode(AVCodecContext *ctx,
                      AVPacket *pkt,
                      AVFrame *frame,
                      QFile &outFile);
    static void aacDecode(const char *inFilename,AudioDecodeSpec &out);

signals:


    // QThread interface
protected:
    virtual void run() override;
};

#endif // AACDECODETHREAD_H

AacDecodeThread.cpp

c 复制代码
#include "aacdecodethread.h"

#include <QDebug>

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

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

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


AacDecodeThread::AacDecodeThread(QObject *parent) : QThread(parent)
{
    // 当监听到线程结束时(finished),就调用deleteLater回收内存
    connect(this, &AacDecodeThread::finished,
            this, &AacDecodeThread::deleteLater);
}

AacDecodeThread::~AacDecodeThread()
{
    // 断开所有的连接
    disconnect();
    // 内存回收之前,正常结束线程
    requestInterruption();
    // 安全退出
    quit();
    wait();
    qDebug() << this << "析构(内存被回收)";
}

int AacDecodeThread::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;
        }

//        for (int i = 0; i < frame->channels; i++) {
//            frame->data[i];
//        }

        // 将解码后的数据写入文件
        outFile.write((char *) frame->data[0], frame->linesize[0]);
    }
}

void AacDecodeThread::aacDecode(const char *inFilename, AudioDecodeSpec &out)
{
    // 返回结果
    int ret = 0;

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

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

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

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

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

    // 获取解码器
    codec = avcodec_find_decoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "decoder not found";
        return;
    }

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

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

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

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

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

    // 打开文件
    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;
    }

    while ((inLen = inFile.read(inDataArray, IN_DATA_SIZE)) > 0) {
        inData = inDataArray;

        while (inLen > 0) {
            // 经过解析器解析
            // 内部调用的核心逻辑是:ff_aac_ac3_parse
            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;
            }
        }
    }
    decode(ctx, nullptr, frame, outFile);

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

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

void AacDecodeThread::run()
{
    AudioDecodeSpec out;
    out.filename = "E:/media/test.pcm";

    aacDecode("E:/media/test.aac", out);

    qDebug() << "采样率:" << out.sampleRate;
    qDebug() << "采样格式:" << av_get_sample_fmt_name(out.sampleFmt);
    qDebug() << "声道数:" << av_get_channel_layout_nb_channels(out.chLayout);
}

注意:本文为个人记录,新手照搬可能会出现各种问题,请谨慎使用

码字不易,如果这篇博客对你有帮助,麻烦点赞收藏,非常感谢!有不对的地方

相关推荐
EasyNVR2 小时前
NVR录像机汇聚管理EasyNVR多品牌NVR管理工具视频汇聚技术在智慧安防监控中的应用与优势
安全·音视频·监控·视频监控
yangshuo12812 小时前
scoop安装ffmpeg转换视频为语音文件
ffmpeg·音视频
hunandede5 小时前
FFmpeg 4.3 音视频-多路H265监控录放C++开发十三.2:avpacket中包含多个 NALU如何解析头部分析
c++·ffmpeg·音视频
声网5 小时前
「人眼视觉不再是视频消费的唯一形式」丨智能编解码和 AI 视频生成专场回顾@RTE2024
人工智能·音视频
Mr.简锋9 小时前
opencv视频读写
人工智能·opencv·音视频
春末的南方城市10 小时前
开源音乐分离器Audio Decomposition:可实现盲源音频分离,无需外部乐器分离库,从头开始制作。将音乐转换为五线谱的程序
人工智能·计算机视觉·aigc·音视频
Hali_Botebie10 小时前
采样率22050,那么CHUNK_SIZE 一次传输的音频数据大小设置多少合适?unity接收后出现卡顿的问题的思路
音视频
风之馨技术录11 小时前
智谱AI清影升级:引领AI视频进入音效新时代
人工智能·音视频
晚点吧12 小时前
视频横屏转竖屏播放-使用人脸识别+目标跟踪实现
人工智能·目标跟踪·音视频
EasyCVR12 小时前
ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
服务器·网络·数据库·音视频