FFmpeg AAC解码实战

上一遍讲了将 PCM 编码成 AAC,那么我们如果想要播放 AAC 文件,需要将 AAC 解码成 PCM,不管是什么播放器,最终都是需要拿到 PCM 数据进行播放的。AAC 解码就是将 AAC 编码后的数据解码成 PCM。

一、FFmpeg 命令行实现 AAC 解码
$ ffmpeg -c:a libfdk_aac -i ar44100ac2s16le.aac -f s16le out.pcm

-i 输入文件
设置输入文件

-c:a libfdk_aac
设置解码器,使用 fdk-aac 解码器;
注意:这个参数是输入参数。

查看我们安装的解码器:

$ ffmpeg -codecs | grep aac
DEAIL. aac                  AAC (Advanced Audio Coding) (decoders: aac aac_fixed aac_at libfdk_aac ) (encoders: aac aac_at libfdk_aac )
D.AIL. aac_latm             AAC LATM (Advanced Audio Coding LATM syntax) 

-f s16le
设置输出的 PCM 文件采样格式。

其他输出参数可以不传,会采用默认值,比如输入文件的采样率是 44100,那么默认输出也是 44100。参数 -f 是必须要有的,否则会报错 Unable to find a suitable output format for ‘out.pcm’ out.pcm: Invalid argument ;要选择解码器支持的采样格式,否则会报错 Requested output format 'XXX' is not a suitable output format out.pcm: Invalid argument

二、FFmpeg 编程实现 AAC 解码

1、解码和编码用到的库是一样的,我们需要引入相关头文件:

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

2、创建所需要的局部变量:

// 返回值
int ret = 0;

QFile inFile(inFilename);
QFile outFile(outFilename);

// 解码器
AVCodec *codec = nullptr;

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

// 解析器上下文  1、和编码不太一样,多了一个解析器上下文
AVCodecParserContext *parserCtx = nullptr;

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

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

// 读取的数据长度(aac)
int inLen = 0;

// 是否是最后一次读取 
int inEnd = 0;

2.1、创建存放读取的 aac 数据缓冲区:

// 输入缓冲区大小
#define AUDIO_INBUF_SIZE 20480

// 存放读取的输入文件数据(aac)
char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
char *inData = inDataArray;

AAC 解码读取数据和 AAC 编码不太一样,AAC 编码是直接把数据读取到 frame 的缓冲区,但是进行 AAC 解码并没有把数据直接放入 AVPacket 缓冲区。建议按照官方的实例程序把数据先读到另外一个地方,也就是先读到我们代码中的 inDataArray 中,所以 inDataArray 中存放的就是读取的输入文件数据,也就是 aac 数据。代码中我们将 inDataArray 的值赋值给了 inData ,那么 inData 就指向了 inDataArray 的首元素。 数组 inDataArray 需要多大呢?可以我们自己来定,代码中我设置的是 20480 字节,数值参考了 FFmpeg 官方示例程序。正常我们认为 inDataArray 的大小直接设置成 20480 就可以了,但是官方示例程序在我们希望的缓冲区大小基础上加了 AV_INPUT_BUFFER_PADDING_SIZE 的大小。为什么要增加 AV_INPUT_BUFFER_PADDING_SIZE 的大小呢?如果你不参考官方示例程序,你在某些地方也是可以发现端倪的。比如在我们后续要用到的一个函数 av_parser_parse2 的注释中有说完整的缓冲区的大小应该被认定为 buffer_size + AV_INPUT_BUFFER_PADDING_SIZE。然后我们查看 AV_INPUT_BUFFER_PADDING_SIZE 源码其值是 64,其中注释说一些优化过的比特流读取器一次读取 32 或者 64 位,有可能会读过头,访问了不该访问的内存空间。为了防止读取器读过头建议你加上 AV_INPUT_BUFFER_PADDING_SIZE。我们可以在 Mac 平台全局搜索(command + shift + F)发现视频解码也有用到 AV_INPUT_BUFFER_PADDING_SIZE

av_parser_parse2 注释
AV_INPUT_BUFFER_PADDING_SIZE
FFmpeg 源码中全局搜索 AV_INPUT_BUFFER_PADDING_SIZE

3、获取解码器:

codec = avcodec_find_decoder_by_name("libfdk_aac");

此处使用的是 libfdk_aaclibfdk_aac 支持的数据格式是 s16,解码出来的 pcm 数据默认也是 s16 采样格式。也可以选择官方或者其他解码器,不同的解码器解码出来的采样格式可能不同。比如 FFmpeg 官方的解码器解码出的 pcm 数据是 fltp 格式。

4、初始化解析器上下文:

parserCtx = av_parser_init(codec->id);

5、创建解码上下文

ctx = avcodec_alloc_context3(codec);

6、创建 AVPacket:

pkt = av_packet_alloc();

7、创建 AVFrame:

frame = av_frame_alloc();

解码创建 AVFrame 和编码不太一样,编码的时候我们设置了一些 frame 参数,开辟了 frame 缓冲区。

8、打开解码器:

ret = avcodec_open2(ctx, codec, nullptr);

options:打开解码器的时候我们可以传递一些解码器参数或者解码器特有参数。

9、打开文件:

inFile.open(QFile::ReadOnly)
outFile.open(QFile::WriteOnly)

10、读取 aac 数据:

inLen = inFile.read(inData, AUDIO_INBUF_SIZE);

11、解析器解析 aac 数据:

ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) {
    ERRBUF(ret);
    qDebug() << "av_parser_parse2 error:" << errbuf;
    goto end;
}

inData += ret;
inLen -= ret;
// 返回已使用的二进制流长度

int av_parser_parse2(AVCodecParserContext *s, // 解析器上下文
                     AVCodecContext *avctx, // 解码上下文
                     uint8_t **poutbuf, // 输出数据地址,此处是 &pkt->data
                     int *poutbuf_size, // 输出数据大小
                     const uint8_t *buf, // 输入数据
                     int buf_size, // 输入数据大小
                     int64_t pts, int64_t dts,
                     int64_t pos);

虽然说 inDataArray 中有 20480 字节的输入数据,但是 parser 并不一定全部“吃得消”,我们把 inDataArray 的首元素地址告诉了parser,parser 会从首元素的位置开始解析数据,解析完把数据送到 AVPacket 中去,然后把 AVPacket 中的数据送入到解码器中解析,解析完成把数据放入 AVFrame 中,最后写入 out.pcm 文件中。所以 inDataArray 中的数据可能会被解析多次(parser 每次解析的长度并不一定相同),这也是创建一个 inData 指针指向 inDataArray 的目的。所以 parser 每解析完一次我们要跳过已解析过的数据,同时 inLenth 要减去已经解析过的数据大小。

parser 解析缓冲区中数据

经过 N 次 parser 解析后,如果 inDataArray 中数据不够了,需要我们从 in.aac 中读取数据填充 inDataArray,那么我们何时重新填充呢?inDataArray 中只剩几个字节数据或者没有了数据的时候我们再填充可不可以? 假如 inDataArray 中只剩几个字节的有效数据,我们照常把 inData 交给 parser,那么 parser 有可能会读取到 AV_INPUT_BUFFER_PADDING_SIZE 的部分,虽然说没有越界,但是 AV_INPUT_BUFFER_PADDING_SIZE 部分是没有有效 aac 数据的,所以显然是不行的。根据官方示例程序,我们需要设置了一个需要再次读取文件数据的阈值(AUDIO_REFILL_THRESH = 4096),当剩余数据长度低于阈值后(inLenth < AUDIO_REFILL_THRESH),我们就要往输入缓冲区 inDataArray 中填充数据。在填充新的数据前,我们需要把输入缓冲区 inDataArray 中剩余的有效数据移动到输入缓冲区 inDataArray 的最前面,然后从 in.aac 文件中读取 aac 数据,填充在剩余有效数据的后面(如下图)。这里需要注意的是,重新从 in.aac 文件中读取的数据长度应该是 AUDIO_INBUF_SIZE - inLenth(缓冲区中剩余有效 aac 数据的长度)。

缓冲区中剩余有效数据长度低于阈值后重新填入数据
// 检查是否需要读取新的文件数据
if (inLen < AUDIO_REFILL_THRESH && !inEnd) {
    // 剩余有效数据移动到缓冲区最前面
    memmove(inDataArray, inData, inLen);

    // 重置inData
    inData = inDataArray;

    // 读取文件数据到inData + inLenth位置
    int len = inFile.read(inData + inLen, AUDIO_INBUF_SIZE - inLen);
    if (len > 0) { // 有读取到文件数据
        inLen += len;
    } else { // 文件中已没有任何数据
        inEnd = 1;
    }

}

12、解码 & 已解码数据写入文件:

static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
    int ret = 0;
    ret = avcodec_send_packet(ctx, pkt);

    if (ret < 0) {
        ERRBUF(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) {
            ERRBUF(ret);
            qDebug() << "avcodec_receive_frame error:" << errbuf;
            return ret;
        }

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

}

注意:如果 frame 中的数据是 planar 格式,写入文件时不能直接写入 frame->data[0],因为可能有多个声道,如果有两个声道,frame->data[0] 是左声道,那么 frame->data[1] 就是右声道。写入 pcm 的数据我们一般不希望是 planar 格式数据,因为播放器最终播放的都不是 planar 格式。以两个声道为例,我们需要轮流把两个声道中的样本数据写入 pcm 文件。具体以声道数为准。

12、刷新缓冲区:

decode(ctx, nullptr, frame, outFile);

或者:

pkt->data = nullptr;
pkt->size = 0;
decode(ctx, pkt, frame, outFile);

可参考 avcodec_send_packet 注释。

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

end:
    inFile.close();
    outFile.close();
    av_packet_free(&pkt);
    av_frame_free(&frame);
    av_parser_close(parserCtx);
    avcodec_free_context(&ctx);
AAC 解码主要流程总结:

1、获取解码器;
2、初始化解析器上下文;
3、创建解码上下文;
4、创建 AVPacket & 创建 AVFrame
5、打开解码器;
6、打开输入输出文件;
7、从 aac 文件中读取压缩数据到输入缓冲区 inDataArray
8、parser 从输入缓冲区 inDataArray 中读取 aac 压缩数据解析,解析完成的压缩数据送到 AVPacket
9、把 AVPacket 发送到解码器;
10、获取解码后的数据,放入 AVFrame
11、解码后的数据写入文件 out.pcm
12、检查输入缓冲区 inDataArray 中剩余数据大小是否低于阈值,如果剩余数据低于阈值,剩余数据移动到输入缓冲区最前面,并且从文件中读取压缩数据,添加到剩余数据后面,重复步骤 8 ~ 12,否则直接重复步骤 8 ~ 12;
13、刷新缓冲区;
14、最后释放资源。

AAC 解码流程

最后不要忘记和 FFmpeg 命令行工具解压缩的生成的 pcm 文件进行大小比对。

av_parser_parse2 源码简单解读:

1、查看 av_parser_parse2

// 源码片段 ffmpeg-4.3.2/libavcodec/parser.c:
int av_parser_parse2(AVCodecParserContext *s, AVCodecContext *avctx,
                     uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size,
                     int64_t pts, int64_t dts, int64_t pos)
{
    int index, i;

    // 省略非核心逻辑代码...

    // 核心逻辑
    index = s->parser->parser_parse(s, avctx, (const uint8_t **) poutbuf, poutbuf_size, buf, buf_size);

    // 省略非核心逻辑代码...

    return index;
}

2、查看 parser_parse,parser 的数据结构是一个结构体,parser_parse 是结构体当中一个指向函数的指针变量,说明 parser_parse 的值是不确定的,是需要赋值的。

// 源码片段 ffmpeg-4.3.2/libavcodec/avcodec.h:
typedef struct AVCodecParser {
    int codec_ids[5]; /* several codec IDs are permitted */
    int priv_data_size;
    int (*parser_init)(AVCodecParserContext *s);
    /* This callback never returns an error, a negative value means that
     * the frame start was in a previous packet. */
    int (*parser_parse)(AVCodecParserContext *s,
                        AVCodecContext *avctx,
                        const uint8_t **poutbuf, int *poutbuf_size,
                        const uint8_t *buf, int buf_size);
    void (*parser_close)(AVCodecParserContext *s);
    int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
    struct AVCodecParser *next;
} AVCodecParser;

3、继续查找 parser_parse 被赋值的地方,我们直接在源码中搜索“parser_parse =”,并不能找到我们想要的结果,我们需要换个思路,查找“parser_parse”在什么地方被赋值,我们发现 parser 是解析器上下文 AVCodecParserContext 中一个变量,并且在 av_parser_parse2 中 s->parser 并没有被赋值,说明在调用 av_parser_parse2 之前 s->parser 已经被赋值。根据经验猜测 parser 是在 av_parser_init 中被赋值,因为 av_parser_init 时我们传入了 codec->id(本文中 codec->id = AV_CODEC_ID_AAC)创建了一个对应的解析器上下文,所以应该是在此时赋值给解析器上下文一个对应的 parser。我们在源码中全局搜索“av_parser_init”,

// 源码片段 ffmpeg-4.3.2/libavcodec/parser.c
AVCodecParserContext *av_parser_init(int codec_id)
{
    AVCodecParserContext *s = NULL;
    const AVCodecParser *parser;
    void *i = 0;
    int ret;

    if (codec_id == AV_CODEC_ID_NONE)
        return NULL;

    // 找到对应的 parser 跳转到 found
    while ((parser = av_parser_iterate(&i))) {
        if (parser->codec_ids[0] == codec_id ||
            parser->codec_ids[1] == codec_id ||
            parser->codec_ids[2] == codec_id ||
            parser->codec_ids[3] == codec_id ||
            parser->codec_ids[4] == codec_id)
            goto found;
    }
    return NULL;

found:
    s = av_mallocz(sizeof(AVCodecParserContext));
    if (!s)
        goto err_out;
    // 此处被赋值
    s->parser = (AVCodecParser*)parser;
    s->priv_data = av_mallocz(parser->priv_data_size);
    if (!s->priv_data)
        goto err_out;
    s->fetch_timestamp=1;
    s->pict_type = AV_PICTURE_TYPE_I;
    if (parser->parser_init) {
        ret = parser->parser_init(s);
        if (ret != 0)
            goto err_out;
    }
    s->key_frame            = -1;

#if FF_API_CONVERGENCE_DURATION
FF_DISABLE_DEPRECATION_WARNINGS
    s->convergence_duration = 0;
FF_ENABLE_DEPRECATION_WARNINGS
#endif

    s->dts_sync_point       = INT_MIN;
    s->dts_ref_dts_delta    = INT_MIN;
    s->pts_dts_delta        = INT_MIN;
    s->format               = -1;
    return s;

err_out:
    if (s)
        av_freep(&s->priv_data);
    av_free(s);
    return NULL;
}

av_parser_iterate 是 parser 迭代器,parser_list 是一个 parser 数组:

// 源码片段 ffmpeg-4.3.2/libavcodec/parsers.c
const AVCodecParser *av_parser_iterate(void **opaque)
{
    uintptr_t i = (uintptr_t)*opaque;
    const AVCodecParser *p = parser_list[i];

    if (p)
        *opaque = (void*)(i + 1);

    return p;
}

// 源码片段 ffmpeg-4.3.2/libavcodec/parser_list.c
static const AVCodecParser * const parser_list[] = {
    &ff_aac_parser,
    &ff_aac_latm_parser,
    &ff_ac3_parser,
    &ff_adx_parser,
    &ff_av1_parser,
    &ff_avs2_parser,
    &ff_bmp_parser,
    &ff_cavsvideo_parser,
    &ff_cook_parser,
    &ff_dca_parser,
    &ff_dirac_parser,
    &ff_dnxhd_parser,
    &ff_dpx_parser,
    &ff_dvaudio_parser,
    &ff_dvbsub_parser,
    &ff_dvdsub_parser,
    &ff_dvd_nav_parser,
    &ff_flac_parser,
    &ff_g723_1_parser,
    &ff_g729_parser,
    &ff_gif_parser,
    &ff_gsm_parser,
    &ff_h261_parser,
    &ff_h263_parser,
    &ff_h264_parser,
    &ff_hevc_parser,
    &ff_jpeg2000_parser,
    &ff_mjpeg_parser,
    &ff_mlp_parser,
    &ff_mpeg4video_parser,
    &ff_mpegaudio_parser,
    &ff_mpegvideo_parser,
    &ff_opus_parser,
    &ff_png_parser,
    &ff_pnm_parser,
    &ff_rv30_parser,
    &ff_rv40_parser,
    &ff_sbc_parser,
    &ff_sipr_parser,
    &ff_tak_parser,
    &ff_vc1_parser,
    &ff_vorbis_parser,
    &ff_vp3_parser,
    &ff_vp8_parser,
    &ff_vp9_parser,
    &ff_webp_parser,
    &ff_xma_parser,
    NULL };

我们查看 parser_list 中 0 号位置的 ff_aac_parser,发现 ff_aac_parser 中 codec_ids 里存放的就是我们要查找的 codec->id,codec_ids 数组大小是 5,说明 parser 最多支持 5种 codec_id,这是 FFmpeg 内部的设计:

// 源码片段 ffmpeg-4.3.2/libavcodec/aac_parser.c
AVCodecParser ff_aac_parser = {
    .codec_ids      = { AV_CODEC_ID_AAC }, // parser支持的codec_id
    .priv_data_size = sizeof(AACAC3ParseContext),
    .parser_init    = aac_parse_init,
    .parser_parse   = ff_aac_ac3_parse,
    .parser_close   = ff_parse_close,
};

所以 av_parser_parse2 最终调用的核心逻辑是 ff_aac_ac3_parse。

// 源码片段 ffmpeg-4.3.2/libavcodec/aac_ac3_parser.c
int ff_aac_ac3_parse(AVCodecParserContext *s1,
                     AVCodecContext *avctx,
                     const uint8_t **poutbuf, int *poutbuf_size,
                     const uint8_t *buf, int buf_size)
{
    AACAC3ParseContext *s = s1->priv_data;
    ParseContext *pc = &s->pc;
    int len, i;
    int new_frame_start;
    int got_frame = 0;

get_next:
    i=END_NOT_FOUND;
    if(s->remaining_size <= buf_size){
        if(s->remaining_size && !s->need_next_header){
            i= s->remaining_size;
            s->remaining_size = 0;
        }else{ //we need a header first
            len=0;
            for(i=s->remaining_size; i<buf_size; i++){
                s->state = (s->state<<8) + buf[i];
                if((len=s->sync(s->state, s, &s->need_next_header, &new_frame_start)))
                    break;
            }

            if(len<=0){
                i=END_NOT_FOUND;
            }else{
                got_frame = 1;
                s->state=0;
                i-= s->header_size -1;
                s->remaining_size = len;
                if(!new_frame_start || pc->index+i<=0){
                    s->remaining_size += i;
                    goto get_next;
                }
                else if (i < 0) {
                    s->remaining_size += i;
                }
            }
        }
    }

    if(ff_combine_frame(pc, i, &buf, &buf_size)<0){
        s->remaining_size -= FFMIN(s->remaining_size, buf_size);
        *poutbuf = NULL;
        *poutbuf_size = 0;
        return buf_size;
    }

    *poutbuf = buf;
    *poutbuf_size = buf_size;

    /* update codec info */
    if(s->codec_id)
        avctx->codec_id = s->codec_id;

    if (got_frame) {
        /* Due to backwards compatible HE-AAC the sample rate, channel count,
           and total number of samples found in an AAC ADTS header are not
           reliable. Bit rate is still accurate because the total frame
           duration in seconds is still correct (as is the number of bits in
           the frame). */
        if (avctx->codec_id != AV_CODEC_ID_AAC) {
            avctx->sample_rate = s->sample_rate;

            if (avctx->codec_id != AV_CODEC_ID_EAC3) {
                avctx->channels = s->channels;
                avctx->channel_layout = s->channel_layout;
            }
            s1->duration = s->samples;
            avctx->audio_service_type = s->service_type;
        }

        if (avctx->codec_id != AV_CODEC_ID_EAC3)
            avctx->bit_rate = s->bit_rate;
    }
    return i;
}

从 FFmpeg 源码中可以发现,parser 没有对数据进行修改,仅对数据做了“切割”,av_parser_parse2 解析完成实际上 pkt->data 和 inData 是指向同一个位置的,pkt->size 保存了解析的数据大小,也就是需要送入解码器的数据大小。

示例代码:

ffmpegutils.h:

#ifndef FFMPEGUTILS_H
#define FFMPEGUTILS_H

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

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

class FFmpegUtils
{
public:
    FFmpegUtils();
    static void aacDecode(const char *inFilename, const char *outFilename, AudioDecodeSpec &out);
};

#endif // FFMPEGUTILS_H

ffmpegutils.cpp:

#include "ffmpegutils.h"

#include <QDebug>
#include <QFile>

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

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

// 输入缓冲区大小
#define AUDIO_INBUF_SIZE 20480

// 需要再次读取输入文件数据的阈值
#define AUDIO_REFILL_THRESH 4096

FFmpegUtils::FFmpegUtils()
{

}

static int decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, QFile &outFile)
{
    int ret = 0;
    ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERRBUF(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) {
            ERRBUF(ret);
            qDebug() << "avcodec_receive_frame error:" << errbuf;
            return ret;
        }

        // 如果 avcodec_send_packet 的数据是planar格式,但是希望写入文件的数据不是planar格式,此处需要修改
        outFile.write((const char *)frame->data[0], frame->linesize[0]);
    }
}

void FFmpegUtils::aacDecode(const char *inFilename, const char *outFilename, AudioDecodeSpec &out)
{
    // 返回值
    int ret = 0;

    QFile inFile(inFilename);
    QFile outFile(outFilename);

    // 解码器
    AVCodec *codec = nullptr;
    // 解码上下文
    AVCodecContext *ctx = nullptr;
    // 解析器上下文  1、和编码不太一样,多了一个解析器上下文
    AVCodecParserContext *parserCtx = nullptr;

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

    // 存放读取的输入文件数据(aac)
    // AV_INPUT_BUFFER_PADDING_SIZE
    char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
    char *inData = inDataArray;

    // 读取的数据长度(aac)
    int inLen = 0;

    // 是否读取到了输入文件的尾部
    int inEnd = 0;

    // 获取解码器
    codec = avcodec_find_decoder_by_name("libfdk_aac");
    if (!codec) {
        qDebug() << "decoder libfdk_aac 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  2、和编码不太一样,编码的时候我们设置了一些 frame 参数,让 frame 有一个缓冲区的大小
    frame = av_frame_alloc();
    if (!frame) {
        qDebug() << "av_frame_alloc error";
        goto end;
    }

    // 打开解码器
    // options 打开解码器的时候我们可以传递一些解码器参数或者解码器特有参数
    ret = avcodec_open2(ctx, codec, nullptr);

    if (ret < 0) {
        ERRBUF(ret);
        qDebug() << "open decoder error:" << errbuf;
        goto end;
    }

    // 打开文件
    if (!inFile.open(QFile::ReadOnly)) {
        qDebug() << "open file failure:" << inFilename;
        goto end;
    }

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

    // 读取数据
    inLen = inFile.read(inData, AUDIO_INBUF_SIZE);

    while (inLen > 0) {
        ret = av_parser_parse2(parserCtx, ctx, &pkt->data, &pkt->size, (const uint8_t *)inData, inLen, AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);

        if (ret < 0) {
            ERRBUF(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 < AUDIO_REFILL_THRESH && !inEnd) {
            memmove(inDataArray, inData, inLen);
            inData = inDataArray;
            int len = inFile.read(inData + inLen, AUDIO_INBUF_SIZE - inLen);
            if (len > 0) {
                inLen += len;
            } else {
                inEnd = 1;
            }
        }
    }

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

方法调用:

#include "mainwindow.h"
#include "ui_mainwindow.h"

#include <QDebug>

#include <ffmpegutils.h>

#define IN_FILE "/Users/mac/Downloads/music/ar44100ac2s16le.aac"
#define OUT_FILE "/Users/mac/Downloads/music/ar44100ac2s16le.pcm"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_decodeAACButton_clicked()
{
    AudioDecodeSpec spec;
    FFmpegUtils::aacDecode(IN_FILE, OUT_FILE, spec);
    qDebug() << "采样率:" << spec.sampleRate;
    qDebug() << "采样格式:" << av_get_sample_fmt_name(spec.sampleFmt);
    qDebug() << "声道数:" << av_get_channel_layout_nb_channels(spec.chLayout);
}

FFmpeg 官方示例:decode_aac.c

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容