ffmpeg 关于音频的思考


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的概念.
视频很自然的用w
h一幅画面来分帧, 音频就比较灵活了.

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 就是把码流文件分割成多个流的对象.

相关推荐
pu_taoc1 小时前
FFmpeg-实战1-解码音频
ffmpeg·音视频
锁我喉是吧21 小时前
Windows mediamtx +ffmpeg电脑推视频流
ffmpeg··rtsp·mediamtx
Industio_触觉智能1 天前
RK3576轻松搭建RTMP视频推流,基于FFmpeg+Nginx协同
nginx·ffmpeg·实时音视频·rtmp·瑞芯微·视频推流·rk3576
你好音视频1 天前
RTSP推流流程深度解析:从协议原理到FFmpeg实现
ffmpeg·音视频
问道飞鱼1 天前
【工具介绍】Ffmpeg工具介绍与简单使用
ffmpeg·视频工具
l***77522 天前
从MySQL5.7平滑升级到MySQL8.0的最佳实践分享
ffmpeg
ZouZou老师2 天前
FFmpeg性能优化经典案例
性能优化·ffmpeg
aqi002 天前
FFmpeg开发笔记(九十)采用FFmpeg套壳的音视频转码百宝箱FFBox
ffmpeg·音视频·直播·流媒体
齐齐大魔王2 天前
FFmpeg
ffmpeg