author: hjjdebug
date: 2025年 12月 01日 星期一 17:28:13 CST
descrip: ffmpeg 关于音频的思考
文章目录
- [1 . 音频frame 是什么意思?](#1 . 音频frame 是什么意思?)
-
- [1.1 音频frame 是多少个采样点?](#1.1 音频frame 是多少个采样点?)
- [2. 原始音频(不需要编码解码的音频)也会对应AVCodecID 吗?](#2. 原始音频(不需要编码解码的音频)也会对应AVCodecID 吗?)
-
- [2.1 原始的音频与AVCodecID 是怎样对应上的?](#2.1 原始的音频与AVCodecID 是怎样对应上的?)
- [3. 原始音频并不需要解码器, 那还有decoder 对象吗?](#3. 原始音频并不需要解码器, 那还有decoder 对象吗?)
-
- [3.1 . 这个decoder对象在哪里定义的?](#3.1 . 这个decoder对象在哪里定义的?)
- [4. 原始音频还需要parser 对象吗?](#4. 原始音频还需要parser 对象吗?)
虽然这里说的是音频, 但对于视频, 也会有类似的问题. 例如原始视频(yuv数据)会不会对应AVCodecID, 是否需要Decoder 对象, 是否需要parser 对象, 答案是类似的.
1 . 音频frame 是什么意思?
视频frame 很好理解, 就是一幅画面 wh 的像素采样点.
音频frame 是什么意思?
数据处理需要分片, 音频也不例外. 每次处理多少数据, 这就是音频frame的概念.
视频很自然的用wh一幅画面来分帧, 音频就比较灵活了.
1.1 音频frame 是多少个采样点?
音频采样点数对应着AVFrame 中的 nb_samples 变量.
常见情况:
AAC编码:通常每帧包含1024个采样点
MP3编码:固定为1152个采样点
其他编码格式:采样点数量可能不同,由具体编解码器来确定
音频的采样点数可由下面公式计算
采样点数= 帧持续时间* 采样率 * 声道数
但是帧持续时间又成了未知数.
2. 原始音频(不需要编码解码的音频)也会对应AVCodecID 吗?
AVCodecID 是编码器,解码器的唯一标识.
原始音频数据(如PCM)并不需要编码,解码, 它会对应AVCodecID 吗?
会的, 此时它并不是为了标识编码,解码,而是为了标识出音频格式.即采样格式.
ffmpeg使用特定的 AVCodecID 来标识PCM 音频流,从0x10000开始, 65536开始
那具体是怎样的对应关系? 在代码中什么位置?
在libavcodec/avcodec_id.h 中
定义了各种 PCM 对应的 CODEC_ID,
/* various PCM "codecs" */
AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
AV_CODEC_ID_PCM_S16LE = 0x10000, //16位有符号整数PCM,小端字节序
AV_CODEC_ID_PCM_S16BE, //16位有符号整数PCM,大端字节序
AV_CODEC_ID_PCM_U16LE,
AV_CODEC_ID_PCM_U16BE,
AV_CODEC_ID_PCM_S8,
AV_CODEC_ID_PCM_U8,
AV_CODEC_ID_PCM_MULAW,
AV_CODEC_ID_PCM_ALAW,
AV_CODEC_ID_PCM_S32LE,
AV_CODEC_ID_PCM_S32BE,
...
2.1 原始的音频与AVCodecID 是怎样对应上的?
首先,原始的音频必定要对应上一个iformat, 而这个format 是人工指定的. 例如命令行参数 -f s16le
有了iformat 的名称, 我们就能够找到这个demux 对象. 例如:ff_pcm_s16le_demuxer,
这个demuxer 对象,里面定义了raw_codec_id. 这也是codecparamter的codec_id
下面看一下这个demuxer 对象具体长啥模样, 挺复杂,我们不关心其它,先关注重要的raw_codec_id成员
cpp
(gdb) p *s->iformat
$6 = {
name = 0x7ffff7ec09fd "s16le",
long_name = 0x7ffff7ec0a08 "PCM signed 16-bit little-endian",
flags = 256,
extensions = 0x7ffff7ec0a28 "sw",
codec_tag = 0x0,
priv_class = 0x7ffff7f38900 <pcm_demuxer_class>,
mime_type = 0x0,
raw_codec_id = 65536, // raw_codec_id, 就等于stream 中 codec_id
priv_data_size = 40,
flags_internal = 0,
read_probe = 0x0,
read_header = 0x7ffff7dd8c10 <pcm_read_header>,
read_packet = 0x7ffff7dd897a <ff_pcm_read_packet>,
read_close = 0x0,
read_seek = 0x7ffff7dd8a5f <ff_pcm_read_seek>,
read_timestamp = 0x0,
read_play = 0x0,
read_pause = 0x0,
read_seek2 = 0x0,
get_device_list = 0x0
}
就是说由demuxer 对象,可以找到raw_codec_id.
结合小标题,就是说给我input_format, 我能找到raw_codec_id.
我们看一下libavformat/pcmdec.c 中代码 read_header 部分
cpp
static int pcm_read_header(AVFormatContext *s)
{
PCMAudioDemuxerContext *s1 = s->priv_data;
AVCodecParameters *par;
AVStream *st;
uint8_t *mime_type = NULL;
int ret;
st = avformat_new_stream(s, NULL);
if (!st)
return AVERROR(ENOMEM);
par = st->codecpar;
par->codec_type = AVMEDIA_TYPE_AUDIO;
par->codec_id = s->iformat->raw_codec_id; // 定义的raw_codec_id 就是par->codec_id
par->sample_rate = s1->sample_rate;
ret = av_channel_layout_copy(&par->ch_layout, &s1->ch_layout);
if (ret < 0)
return ret;
par->bits_per_coded_sample = av_get_bits_per_sample(par->codec_id);
par->block_align = par->bits_per_coded_sample * par->ch_layout.nb_channels / 8;
avpriv_set_pts_info(st, 64, 1, par->sample_rate);
return 0;
}
就是说, 经read_header 之后,demuxer 中的raw_codec_id, 就赋值给了 AVCodecParameters 中的codec_id
codec_id 有什么用呢? 由codec_id 可以找到decoder 对象.
3. 原始音频并不需要解码器, 那还有decoder 对象吗?
avcodec_find_decoder(AV_CODEC_ID_PCM_U8) 会返回什么结果呢?
马上写段代码测试一下, 谁说都不如计算机说的准确.
cpp
#include <stdio.h>
#include <libavcodec/avcodec.h>
int main()
{
const AVCodec *decoder= avcodec_find_decoder(AV_CODEC_ID_PCM_U8);
printf("decoder:%p\n",decoder);
return 0;
}
结果: 真的对应decoder 对象,而不是想象中的NULL
cpp
(gdb) p *decoder
$3 = {
name = 0x7ffff75e1d28 "pcm_u8",
long_name = 0x7ffff75e1d2f "PCM unsigned 8-bit",
type = AVMEDIA_TYPE_AUDIO,
id = AV_CODEC_ID_PCM_U8, // decoder 中是有 id 字段的
capabilities = 16386,
max_lowres = 0 '\000',
supported_framerates = 0x0,
pix_fmts = 0x0,
supported_samplerates = 0x0,
sample_fmts = 0x7ffff75e1d48 <__compound_literal.42>,
channel_layouts = 0x0,
priv_class = 0x0,
profiles = 0x0,
wrapper_name = 0x0,
ch_layouts = 0x0
}
3.1 . 这个decoder对象在哪里定义的?
p p
$5 = (const AVCodec *) 0x7ffff78a98a0 <ff_pcm_u8_decoder>
查其地址0x7ffff78a98a0: 属于libavcodec.so 中定义的对象.
用grep 在代码中查询 ff_pcm_u8_decoder
只查到对它的引用, 但没有直接查到它的定义. 看来其定义是被宏之类给掩盖了.
那么它到底在那个文件中定义的呢?
我们查"PCM unsigned 8-bit", 在以下4个文件中发现了它, 所以根据你关心的领域,去看相应的代码.
libavcodec/pcm.c:619:PCM_CODEC (PCM_U8, AV_SAMPLE_FMT_U8, pcm_u8, "PCM unsigned 8-bit");
libavcodec/codec_desc.c:2004: .long_name = NULL_IF_CONFIG_SMALL("PCM unsigned 8-bit"),
libavformat/pcmenc.c:64:PCMDEF(u8, "PCM unsigned 8-bit", "ub", U8)
libavformat/pcmdec.c:175:PCMDEF(u8, "PCM unsigned 8-bit", "ub", U8)
实际上ff_pcm_u8_decoder 是在libavcodec/pcm.c 中定义的 , 用宏定义了一堆对象, 用 $nm libavcodec/pcm.o 可以验证
所谓宏, 就是嫌代码写的太繁了, 所以定义了一个宏代表代码模板, 这样一个宏就能代表很多行代码.
decoder 对象有什么用?
decoder 对象就是对packet数据进行解码形成frame 的
不过它的pcm_decode_frame 并不需要解码, 而是直接copy 数据. 同样编码器对象也是一样
4. 原始音频还需要parser 对象吗?
parser 对象是packet 到frame 的辅助对象.
当你从上层调用av_read_frame 时, 还用不用parser 对象呢?
答案是: 原始音频不需要parser 就可以直接成帧的, 这根直观感觉是一致的.
AVCodecParserContext 对象生成是这样调用的,
AVCodecParserContext *av_parser_init(int codec_id)
假如给它一个PCM的codec_id, 它会找到什么样的parser 呢.
AVCodecParserContext *av_parser_init(AV_CODEC_ID_PCM_U8)
写代码测试一下:, 并跟踪调试一下代码, 确认没有这样的parser, 返回值是NULL
cpp
$ cat main.c
#include <stdio.h>
#include <libavcodec/avcodec.h>
int main()
{
AVCodecParserContext *ctx = av_parser_init(AV_CODEC_ID_PCM_U8);
printf("ctx:%p\n",ctx);
return 0;
}
如果我们一定要从av_read_frame 中跟踪一下呢?它是在read_frame_internal()中根据sti->need_parsing 做出的判断
cpp
int read_frame_internal(AVFormatContext* s, AVPacket* pkt) 中
有
需要parser 属于下面情况
if (sti->need_parsing && !sti->parser && !(s->flags & AVFMT_FLAG_NOPARSE))
{
sti->parser = av_parser_init(st->codecpar->codec_id);
...
}
无parser 属于下面的情况.
if (!sti->need_parsing || !sti->parser)
{
//不需要parse, 只需要直接填充一下pkt 的各个成员变量.
compute_pkt_fields(s, st, NULL, pkt, AV_NOPTS_VALUE, AV_NOPTS_VALUE);
if ((s->iformat->flags & AVFMT_GENERIC_INDEX) && (pkt->flags & AV_PKT_FLAG_KEY) && pkt->dts != AV_NOPTS_VALUE)
{
// 该函数是把formatcontext s 中特定的流 st的索引条目太多时, 减少索引条目至一半, 方法是只保留偶数索引条目.
ff_reduce_index(s, st->index);
av_add_index_entry(st, pkt->pos, pkt->dts, 0, 0, AVINDEX_KEYFRAME); //添加一个条目, 条目就是pos,dts, flag等信息
}
got_packet = 1;
}
其中 sti->need_parsing 这个参数,默认是假,除非根据流类型,
demuxer 在read_header 中创建流之后,设置其need_parsing为真.
ok, 如此解释清楚了demuxer 中的工作过程, 直接阅读代码会帮助你理解. 这是记录了我的思考过程.
顺便说一句, demuxer 就是把码流文件分割成多个流的对象.