ffmpeg 与 aac 文件解码


author: hjjdebug

date: 2025年 12月 05日 星期五 09:33:57 CST

descrip: ffmpeg 与 aac 文件解码


文章目录

  • [1. aac 文件对应的 demuxer 对象](#1. aac 文件对应的 demuxer 对象)
  • [2. aac 文件对应的 parser对象](#2. aac 文件对应的 parser对象)
    • [2.1. 对parser的访问:](#2.1. 对parser的访问:)
    • [2.2 aac_parse_init](#2.2 aac_parse_init)
    • [2.3 ff_aac_ac3_parse](#2.3 ff_aac_ac3_parse)
      • [2.3.1 ff_adts_header_parse 函数](#2.3.1 ff_adts_header_parse 函数)
  • [3. aac 文件对应的decoder对象](#3. aac 文件对应的decoder对象)
    • [3.1 aac_decode_init](#3.1 aac_decode_init)
    • [3.2 aac_decode_frame](#3.2 aac_decode_frame)

测试文件. 1.aac, 是一个单通道aac 格式音频文件.
测试程序. 随便写一个简单的测试程序吧,
能调用到avcodec_send_packet(),avcodec_receive_frame()就行
用附带的demux-decoding 程序就很好.

以前用c写代码,2个人合作常说, 你写个函数让我调一调, 我就省事了.

现在写代码会说, 你写个类,让我调一调. 或者给我个对象,让我调用调用.

对象好啊, 不仅可以包含成员,还可以包含函数, 一块打包了. 接口更简单了.

针对aac 文件格式的解码, ffmpeg 已经给我们提供了几个对象.

至于编码, 那是反方向, 以后再说.

1. aac 文件对应的 demuxer 对象

对象名称为 ff_aac_demuxer ,类型 AVInputFormat

注意: AVInputFormat 不是一个AVClass 类 但它可以包含一个私有的AVClass 类指针

例如 ff_pcm_s16le_demuxer 原始pcm 格式demux 就包含一个私有AVClass 指针,

.priv_class = &pcm_demuxer_class

.priv_data_size = sizeof(PCMAudioDemuxerContext)

其中在PCMAudioDemuxerContext中定义的选项sample_rate, ch_layout 可以从外部去设置,

例如从命令行设置.

好在ff_aac_demuxer 不需要包含私有类对象, 其.priv_data_size, .priv_class 均为0

demuxer 对象可以用ffmpeg -h的帮助系统列出对象属性

例如: $ ffmpeg -h demuxer=aac

显示如下:

cpp 复制代码
Demuxer aac [raw ADTS AAC (Advanced Audio Coding)]:
    Common extensions: aac.

解释一下它的运行过程:

-h 对应着ffmpeg options 中的全局选项, demuxer=aac 是它的参数.

-h 选项会执行show_help() 函数, 带demuxer=aac参数会进一步调用到show_help_demuxer("aac")

show_help_demuxer(char *name) 函数如下, 执行之则输出了上述结果.

cpp 复制代码
 void show_help_demuxer(const char *name)
{
    const AVInputFormat *fmt = av_find_input_format(name); //关键函数,由名称找到对象,后面只是打印对象的属性
    printf("Demuxer %s [%s]:\n", fmt->name, fmt->long_name);
    if (fmt->extensions)
        printf("    Common extensions: %s.\n", fmt->extensions);
    if (fmt->priv_class) //如果有子类,还会打印子类的属性, ff_aac_demuxer 就没有子类对象了.
        show_help_children(fmt->priv_class, AV_OPT_FLAG_DECODING_PARAM);
}

干脆给出 ff_aac_demuxer 对象定义吧.

在libavformat/aacdec.c中定义,

cpp 复制代码
const AVInputFormat ff_aac_demuxer = {
    .name = "aac",
    .long_name = NULL_IF_CONFIG_SMALL("raw ADTS AAC (Advanced Audio Coding)"),
    .read_probe = adts_aac_probe,
    .read_header = adts_aac_read_header,
    .read_packet = adts_aac_read_packet,
    .flags = AVFMT_GENERIC_INDEX,
    .extensions = "aac",
    .mime_type = "audio/aac,audio/aacp,audio/x-aac",
    .raw_codec_id = AV_CODEC_ID_AAC,
};

aacdec.c 文件很小,内容是比较简单的.

但是请注意, 这个对象是不能直接从外边访问的, 因为它的符号类型不是全局的,而是私有的.

$ nm libavformat.so|grep ff_aac_demuxer

0000000000309080 d ff_aac_demuxer

是小写的'd',而不是大写的'D'

本来.o文件符号还是公有的,链接成.so文件时被脚本链接成了私有符号,不向外公开了.

不过gdb 功能倒是很强大, 它不管你是全局数据还是私有数据, 一样都可以打印出来.甚慰!

它的内存地址也能一目了然,不用推算了. 为我们调试提供了方便.

那外部怎样访问这个对象呢? 只能通过已经提供的接口.

例如:

const AVInputFormat *av_demuxer_iterate(void **opaque)

int i=0;

while ((fmt = av_demuxer_iterate(&i) //就能访问到所有的demuxer.

av_demuxer_iterate 函数可以访问demuxer 对象地址数组,根据传入参数i,访问指定对象.

例如:avformat_open_input() 就通过

av_probe_input_format2() 把探测到的格式保存到了 s->iformat 成员变量中

找的过程需要枚举每一种格式,找到对象的依据是使用探测数据看是否符合你的格式要求,

你找到对象后,要保留对象地址以被以后使用.

av_find_input_format(name)

根据名称找demuxer 对象是另一个接口,也会调用iterate函数

aacdec.c文件主要功能是提交格式探测函数 和 读文件头和读包函数

文件格式探测:

static int adts_aac_probe(const AVProbeData* p)

先找frame 的开头0xfff(12个1) 再从固定位置读取本frame的长度

偏移之后又能遇到0xfff, 再读取frame_size, 再继续向下找.

在探测的数据中能够找到3个为合格.

读文件头:

static int adts_aac_read_header(AVFormatContext* s)

该函数没干什么事,只是创建了一个音频流. 并且把数据读指针同步到0xfff开始的地方.

读包函数:

static int adts_aac_read_packet(AVFormatContext* s, AVPacket* pkt)

就是把从包头0xfff 到frame_size的数据都读出来.

这三个函数都声明为static 了, 因而也都是私有的,外部函数不能直接调用它们.

$ nm libavformat.so|grep adts_aac_probe

000000000004031c t adts_aac_probe

nm libavformat.so|grep adts_aac_read_header

00000000000405ea t adts_aac_read_header

对这三个函数的使用, 也只能通过ff_aac_demuxer对象来调用.

不过你可以用gdb 在这些函数上下断点, 就能轻松知道它的调用栈,方便在ffmpeg环境下调试代码,了解它们的功能.

AAC文件是由一个个ADTS frame构成的, 每个frame都有7字节frame 头和数据构成.

demuxer 只是访问了头部中56bits的12个bit同步位和frame_length(13bits).

数据体还没涉及, 内容真的很少.

2. aac 文件对应的 parser对象

对象名称为 ff_aac_parser , 类型 AVCodecParser

注意: AVCodecParser 并不是一个AVClass 类, 意味着它没有什么外部可设置项,

干脆把对象copy过来吧

对象在libavcodec/aac_parser.c 文件中

cpp 复制代码
const AVCodecParser ff_aac_parser = {
    .codec_ids = { AV_CODEC_ID_AAC },
    .priv_data_size = sizeof(AACAC3ParseContext), //可以有私有数据,但私有数据不是avclass对象
    .parser_init = aac_parse_init,
    .parser_parse = ff_aac_ac3_parse,
    .parser_close = ff_parse_close,
};

它的parser_parse 函数ff_aac_ac3_parse 在libavcodec/aac_ac3_parser.c中

其中调用到ff_adts_header_parse() 在libavcodec/adts_header.c 中

2.1. 对parser的访问:

是通过枚举来进行的, 给不同的i,将返回不同的对象地址.

while ((parser = av_parser_iterate(&i)))

av_parser_init 函数介绍

AVCodecParserContext *av_parser_init(int codec_id)

该函数功能是根据codec_id 找到parser, 找的过程就是前面的那个枚举,

对每一个对象,进行如下判断:

if (parser->codec_id[0] == codec_id) goto find;
按图索骥, 那么多对象,总要给点线索,才能给你返回正确的东西.

然后创建一个AVCodecParserContext, 返回.

其中codec_id 可以由demuxer 提供, 当你找到demuxer 时,其中包含codec_id 信息.

2.2 aac_parse_init

该函数是parser 的 parser_init 函数指针, 可以直接下断点调试跟踪

这个函数太简单,就是给私有结构s赋值. 抄过来以观其貌.

cpp 复制代码
static av_cold int aac_parse_init(AVCodecParserContext *s1)
{
   AACAC3ParseContext *s = s1->priv_data;
   s->header_size = AV_AAC_ADTS_HEADER_SIZE;
   s->sync = aac_sync;
   return 0;
}

2.3 ff_aac_ac3_parse

该函数是parser 的 parser_parse 函数指针, 可以直接下断点调试跟踪

首先分析一下里面的 s->sync 指针函数, 就是初始化是赋值的 aac_sync() 函数, 其中又调用了

ff_adts_header_parse 函数

2.3.1 ff_adts_header_parse 函数

该函数完整解析了头部56bits 数据, 7bytes 头部.

代码如下, 功能是试图获取hdr 数据, 成功返回frame_size, 失败返回错误码(负数)

cpp 复制代码
int ff_adts_header_parse(GetBitContext *gbc, AACADTSHeaderInfo *hdr)
{
    int size, rdb, ch, sr;
    int aot, crc_abs;

    memset(hdr, 0, sizeof(*hdr));

    if (get_bits(gbc, 12) != 0xfff)
        return AAC_AC3_PARSE_ERROR_SYNC;

    skip_bits1(gbc);             /* id */
    skip_bits(gbc, 2);           /* layer */
    crc_abs = get_bits1(gbc);    /* protection_absent */
    aot     = get_bits(gbc, 2);  /* profile_objecttype */
    sr      = get_bits(gbc, 4);  /* sample_frequency_index */
    if (!ff_mpeg4audio_sample_rates[sr])
        return AAC_AC3_PARSE_ERROR_SAMPLE_RATE;
    skip_bits1(gbc);             /* private_bit */
    ch = get_bits(gbc, 3);       /* channel_configuration */

    skip_bits1(gbc);             /* original/copy */
    skip_bits1(gbc);             /* home */

    /* adts_variable_header */
    skip_bits1(gbc);             /* copyright_identification_bit */
    skip_bits1(gbc);             /* copyright_identification_start */
    size = get_bits(gbc, 13);    /* aac_frame_length */
    if (size < AV_AAC_ADTS_HEADER_SIZE)
        return AAC_AC3_PARSE_ERROR_FRAME_SIZE;

    skip_bits(gbc, 11);          /* adts_buffer_fullness */
    rdb = get_bits(gbc, 2);      /* number_of_raw_data_blocks_in_frame */

    hdr->object_type    = aot + 1;
    hdr->chan_config    = ch;       //通道配置,决定了通道数
    hdr->crc_absent     = crc_abs;
    hdr->num_aac_frames = rdb + 1;  //一般是1
    hdr->sampling_index = sr;
    hdr->sample_rate    = ff_mpeg4audio_sample_rates[sr]; //采用率,一般是44100或48000
    hdr->samples        = (rdb + 1) * 1024;  // 
    hdr->bit_rate       = size * 8 * hdr->sample_rate / hdr->samples; //比特率的计算
    hdr->frame_length   = size;  //本frame 的大小

    return size;
}

理解了get_bits() 函数,上面代码就能读懂了.

这里给一个7byte的header 实例:

FF F1 4C 00 B8 1F FC

  1. hdr 参数

    从7bytes 头中提取的参数, num_aac_frames 一般就是1个frame, samples 采样点数就是1024

    还包含了通道配置ch, 确定了channel 个数或者说ch_layout

    采样率(sample_rate),一般是44.1k或48k

    object_type, 大概是确定了采样点格式.

    上面3要素是播放所必需的.

    frame_length说明了本压缩数据的长度,它代表了本帧的压缩数据长度,它能解出1024个采样点.

    于是我们可以计算出比特率

  2. 比特率的计算:

    hdr->bit_rate = size * 8 * hdr->sample_rate / hdr->samples;

    hdr->sample_rate: 采样率,每秒的采样点数, 通常是44.1k,或48k

    hdr-> samples: 数据块中的采用点数, 1024个采样点

    二者相除,代表1秒采集了多少块,

    size 是本块数据的byte 大小,

    * 8 变成bit大小

    1秒采集n块,一块size大小, 2者相乘再乘8就是比特率大小.

ff_aac_ac3_parse 函数比较长,就不copy 了, 跟踪它还可以看懂,

基本功能是找到帧头后,记录下帧长度,设置循环退出条件,并设置输出数据

里边代码有一些过渡冗余,好几次调用header_parse, 只是为了健壮性.

总的来讲, parser 函数也只是涉及到帧头的解析, 并没有真正解码帧数据.

对数据的解压缩,那要看decoder 了.

3. aac 文件对应的decoder对象

由codec_id 可以找到decoder, codec_id 就是demuxer中使用的AV_CODEC_ID_AAC.

用法:

AVCodec *dec = avcodec_find_decoder(AV_CODEC_ID_AAC);

我们还是看看它的真身在哪里吧,调试知:

源码位于: libavcodec/aacdec.c

其对象名称为: ff_aac_decoder, 对象类型 AVCodec, 严格说是FFCodec,

AVCodec是FFCodec 的子类 .p 部分, public 部分.

cpp 复制代码
const FFCodec ff_aac_decoder = {
    .p.name          = "aac",
    CODEC_LONG_NAME("AAC (Advanced Audio Coding)"),
    .p.type          = AVMEDIA_TYPE_AUDIO,
    .p.id            = AV_CODEC_ID_AAC,
    .priv_data_size  = sizeof(AACContext),
    .init            = aac_decode_init,
    .close           = aac_decode_close,
    FF_CODEC_DECODE_CB(aac_decode_frame),    //最重要函数!!!
    .p.sample_fmts   = (const enum AVSampleFormat[]) {
        AV_SAMPLE_FMT_FLTP, AV_SAMPLE_FMT_NONE
    },
    .p.capabilities  = AV_CODEC_CAP_CHANNEL_CONF | AV_CODEC_CAP_DR1,
    .caps_internal   = FF_CODEC_CAP_INIT_CLEANUP,
    CODEC_OLD_CHANNEL_LAYOUTS_ARRAY(aac_channel_layout)
    .p.ch_layouts    = aac_ch_layout,
    .flush = flush,
    .p.priv_class    = &aac_decoder_class,
    .p.profiles      = NULL_IF_CONFIG_SMALL(ff_aac_profiles),
};

下面介绍2个重要函数: 这些函数都在libavcodec/aacdec_template.c文件中,3480行代码

3.1 aac_decode_init

如果你单步跟踪下去,连子函数也单步跟踪,会累到手抽筋, 没看出所以然. 反正在做一堆设置.

3.2 aac_decode_frame

碰到了一堆新概念,新名称,翻一翻代码,这是具体解压缩数据的地方.

一般我们无需知道具体算法, 有需求再研究. 已经兵临城下.

相关推荐
八月的雨季 最後的冰吻2 小时前
FFmepg-- 30-ffplay源码解析-read_thread 的引用计数
ffmpeg·音视频
xiaoqi9766336903 小时前
免费文字转语音助手 python+edge_tts+FFMPEG
python·edge·ffmpeg
mortimer1 天前
Python + FFmpeg 视频自动化处理指南:从硬件加速到精确剪辑
python·ffmpeg·音视频开发
小c君tt1 天前
ffmpeg-音-视频-基本概念
ffmpeg·音视频
Hello World,1 天前
使用ffmpeg播放视频并添加当前时间水印
ffmpeg·音视频
筏.k1 天前
WebRTC 集成 FFmpeg D3D12VA HEVC 硬件编码 avcodec_open2 返回 -22 问题排查与解决方案
ffmpeg·webrtc
苏三福2 天前
摄像头推流、拉流
ffmpeg
你好音视频2 天前
RTSP拉流:RTP包解析流程详解
ffmpeg·音视频
大熊背2 天前
PotPlay视频播放器YUV色彩空间不一致所导致的图像发蒙问题及优化方案
ffmpeg·色彩空间·通透度