音视频-AAC解码

解码的大致逻辑和编码的反着来:

AAC源文件 ==> (AVPacket)输入缓冲区 ==>  (AVCodec)解码器 ==> (AVFrame)输出缓冲区 ==> 输出文件

对于FFMPEG解码音视频的一般来讲,都是直接从媒体容器文件(网络码流或者封装文件)中,读取出AVPaket传给解码器。但一般音视频解码并不是在这样的场景下,而是直接给解码器传送裸码流(AAC、h264等),此时我们需要知道每次传给解码器的音视频数据大小,即每帧音频/视频大小。AVCodecParser可通过音视频裸码流解析出每帧的大小等信息。

也就是

AAC源文件 ==> AVCodecParser  ==> AVPacket ==> 解码器 ==> AVFrame ==> 输出文件

Win环境下, 使用ffmpeg解码 :

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

Mac环境下, 使用ffmpeg解码 :

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

in.aac 输入aac文件
out.pcm aac解码后得到的pcm文件

核心函数AVCodecParser

/**
 * Parse a packet.
 *
 * @param s             parser context.
 * @param avctx         codec context.
 * @param poutbuf       set to pointer to parsed buffer or NULL if not yet finished.
 * @param poutbuf_size  set to size of parsed buffer or zero if not yet finished.
 * @param buf           input buffer.
 * @param buf_size      buffer size in bytes without the padding. I.e. the full buffer
                        size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE.
                        To signal EOF, this should be 0 (so that the last frame
                        can be output).
 * @param pts           input presentation timestamp.
 * @param dts           input decoding timestamp.
 * @param pos           input byte position in stream.
 * @return the number of bytes of the input bitstream used.
 * @return 输入流用了多少返回多少字节
 *
 * Example:
 * @code
 *   while(in_len){
 *       len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
 *                                        in_data, in_len,
 *                                        pts, dts, pos);
 *       in_data += len;
 *       in_len  -= len;
 *
 *       if(size)
 *          decode_frame(data, size);
 *   }
 * @endcode
 */

解码需要一个解析器, 这个解析器读取aac文件中的数据, 传递到AVPacket中, 也就是packet->data , 和packet->size

 * @param poutbuf       set to pointer to parsed buffer or NULL if not yet finished.
 * @param poutbuf_size  set to size of parsed buffer or zero if not yet finished.


代码实现 Demo

#include "aacDecodeThread.h"

#include <QDebug>
#include <QFile>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/channel_layout.h>
#include <libavutil/common.h>
#include <libavutil/frame.h>
#include <libavutil/samplefmt.h>
}

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


#define CHECK_IF_ERROR_BUF_END(ret, funcStr) \
    if (ret) { \
        ERROR_BUF(ret); \
        qDebug() << #funcStr << " error :" << errbuf; \
        goto end; \
    }



#ifdef Q_OS_WIN
    #define IN_AAC_FILEPATH "G:/Resource/pcm_to_aac.aac"
    #define OUT_PCM_FILEPATH "G:/Resource/aac_decode_to_pcm.pcm"
#else
    #define IN_AAC_FILEPATH "/Users/liliguang/Desktop/pcm_to_aac.aac"
    #define OUT_PCM_FILEPATH "/Users/liliguang/Desktop/aac_decode_to_pcm.pcm"
#endif

#define AUDIO_INBUF_SIZE 20480
#define AUDIO_REFILL_THRESH 4096

// 音频解码
// 返回负数:中途出现了错误
// 返回0:解码操作正常完成
static int decode(AVCodecContext *ctx,
                  AVFrame *frame,
                  AVPacket *pkt,
                  QFile &outFile) {

    // 发送数据到解码
    int ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        ERROR_BUF(ret);
        qDebug() << "avcodec_send_frame error" << errbuf;
        return ret;
    }

    // 从解码器中获取到数据到frame
    ret = avcodec_receive_frame(ctx, frame);
    if (ret == AVERROR(EAGAIN) ) {
        qDebug() << "ret == AVERROR(EAGAIN)" << ret ;
        return ret;
    } else if (ret == AVERROR_EOF) {
        // 全部获取完毕
        qDebug() << "ret == AVERROR_EOF" << ret  ;

        return ret;
    } else if (ret < 0) {
        qDebug() << "ret < 0" << ret ;

        return ret;
    }
    qDebug() << "ret == 0" << ret ;

    // 成功从编码器拿到编码后的数据
    // 将编码后的数据写入文件
    outFile.write((char *) frame->data[0], frame->linesize[0]);
    return 0;
}



void AACDecodeThread::run() {
    qDebug() << "AACEncodeThread run ";

    // 输入输出文件
    const char *infilename;
    const char *outfilename;

    // 解码器
    const AVCodec *codec = nullptr;
    // 解码器上下文
    AVCodecContext *codecCtx = nullptr;

    // FFMPEG解码音视频的一般来讲,都是直接从媒体容器文件(网络码流或者封装文件)中,读取出AVPaket传个解码器。
    // 但一般音视频解码并不是在这样的场景下,而是直接给解码器传送裸码流(AAC、h264等),
    // 此时我们需要知道每次传给解码器的音视频数据大小,即每帧音频/视频大小。
    // AVCodecParser可通过音视频裸码流解析出每帧的大小等信息。

    //解析器上下文
    AVCodecParserContext *codecParserCtx = nullptr;

    // 源文件数据源存储结构指针
    AVFrame *frame;
    // 编码文件数据源存储结构指针
    AVPacket *pkt;

    int avcodec_open2_Ret;

    int infileOpen_Ret;
    int outfileOpen_Ret;

    infilename = IN_AAC_FILEPATH;
    outfilename = OUT_PCM_FILEPATH;

    QFile inFile(infilename);
    QFile outFile(outfilename);

    // 加上AV_INPUT_BUFFER_PADDING_SIZE是为了防止某些优化过的reader一次性读取过多导致越界.
    char inDataArray[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];   // 输入缓冲区
    char *inData = inDataArray;                                          // 指向输入缓冲区指针
    int inDataLen = 0; // 读取到文件的数据大小
    bool readMoreReachEnd = false;// 加载更多文件数据是否已经结束

    int inParserRet = 0;

    int decode_ret;
    // ============================================================
    // 解码逻辑  源文件 ==> 解析器 ==> (AVPacket)输入缓冲区 ==> 解码器 ==> (AVFrame)输出缓冲区 ==> 输出文件

    // 输入文件
    infileOpen_Ret = inFile.open(QFile::ReadOnly);
    CHECK_IF_ERROR_BUF_END(!infileOpen_Ret, "inFile.open");
    // 输出文件
    outfileOpen_Ret = outFile.open(QFile::WriteOnly);
    CHECK_IF_ERROR_BUF_END(!outfileOpen_Ret, "outFile.open");

    // 解码器
    codec = avcodec_find_decoder_by_name("libfdk_aac");
    CHECK_IF_ERROR_BUF_END(!codec, "avcodec_find_decoder");

    // 解析器上下文
    codecParserCtx = av_parser_init(codec->id);
    CHECK_IF_ERROR_BUF_END(!codecParserCtx, "av_parser_init");

    // 解码器上下文
    codecCtx = avcodec_alloc_context3(codec);
    CHECK_IF_ERROR_BUF_END(!codecCtx, "avcodec_alloc_context3");

    // 打开解码器
    avcodec_open2_Ret = avcodec_open2(codecCtx, codec, nullptr);
    CHECK_IF_ERROR_BUF_END(avcodec_open2_Ret, "avcodec_open2");

    // 创建输入Packet
    pkt = av_packet_alloc();
    CHECK_IF_ERROR_BUF_END(!pkt, "av_packet_alloc");

    // 创建输出rame
    frame = av_frame_alloc();
    CHECK_IF_ERROR_BUF_END(!frame, "av_frame_alloc");


    // 读取文件到解析器中。
    // 解析完成之后存放到pkt
    // pkt送到解码器
    // 输出

    // 先读取一次, 如果有值,解析数据
    //
    //  总大小 20480 + AV_INPUT_BUFFER_PADDING_SIZE
    //  -------------------------------
    //  | 【20480 】 +【64】  |
    //  -------------------------------
    //  每次读取4096, 4096的内容中, 用解析器解析, 解析器每次解析180-200左右,直到解析完毕
    // inDataLen 每次读取文件的长度

    inDataLen = inFile.read(inDataArray, AUDIO_INBUF_SIZE);
    CHECK_IF_ERROR_BUF_END(inDataLen <= 0, "inFile.read");

    inData = inDataArray;

    // 每一次解析内容大小
    while(inDataLen > 0) {
        //the number of bytes of the input bitstream used.
        inParserRet = av_parser_parse2(codecParserCtx,
                                       codecCtx,
                                       &pkt->data,
                                       &pkt->size,
                                       (uint8_t *)inData,//输入缓冲区。
                                       inDataLen,//buf_size + AV_INPUT_BUFFER_PADDING_SIZE。
                                       AV_NOPTS_VALUE,
                                       AV_NOPTS_VALUE,
                                       0);

        CHECK_IF_ERROR_BUF_END(inParserRet < 0, "av_parser_parse2");

        // 指针位置偏移,跳过已经解析的数据
        inData += inParserRet;
        //读取的大小减去已经解析的大小
        inDataLen  -= inParserRet;

        if ( inParserRet > 0) {
            decode_ret = decode(codecCtx, frame, pkt, outFile);
            CHECK_IF_ERROR_BUF_END(  decode_ret < 0, "decode_ret");
        }


        // 每次读取 200左右, 如果当前已经小于AUDIO_REFILL_THRESH, 那就重新读取一遍文件
        if (inDataLen < AUDIO_REFILL_THRESH && !readMoreReachEnd) {
            memmove(inDataArray, inData, inDataLen);

            inData = inDataArray;

            // 读取新的数据到后面
            int readMoreLen = inFile.read(inDataArray + inDataLen,  AUDIO_INBUF_SIZE - inDataLen );
            qDebug() << "readMoreLen" << readMoreLen;
            if ( readMoreLen > 0 ) {
                inDataLen += readMoreLen;
                readMoreReachEnd = false;
            } else {
                readMoreReachEnd = true;
            }

        }
    }

    qDebug() << "AACEncodeThread while end  ";


    // 冲刷最后一次缓冲区
    pkt->data = NULL;
    pkt->size = 0;
    decode_ret = decode(codecCtx, frame, nullptr, outFile);
    qDebug() << "AACEncodeThread Last Decode ";

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

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

    avcodec_free_context(&codecCtx);
    av_parser_close(codecParserCtx);
    qDebug() << "AACEncodeThread end ";
}

代码的大体逻辑和编码的很像,因为是发过来而已, 这里值得注意的是, 读取文件的逻辑和编码不一样。

值得注意的是:源文件record_to_pcm.pcm==> 编码 pcm_to_aac.aac ==> 解码aac_decode_to_pcm.pcm, 经过了一轮编解码之后, 最后的PCM文件的大小会有发生变化, 这个跟比特率的设置有关系。


总结 :

AAC编码的简略逻辑 : 源文件 ==》 AVPacket ==》编码器 ==》AVFrame ==> 输出文件

细致化:

源文件

inFile.open(QFile::ReadOnly) 打开文件读取文件内容到缓冲区

Parser 解析器读取输入文件缓冲区的内容, 读取后传给AVPacket

AVPacket 发送处理avcodec_send_packet

AVCodec 解码器

AVPacket 接受处理avcodec_receive_frame

outFile.open(QFile::WriteOnly) 从缓冲区读取数据写入文件中去

输出文件

编码完成

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

推荐阅读更多精彩内容