一、引言
从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以显示WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的size:
这个“size”实际是AVPacket结构体中的成员变量size,为WAV音频文件中某个packet的大小(单位为字节),通过fftools/ffprobe.c中的show_packet函数打印出来:
static void show_packet(WriterContext *w, InputFile *ifile, AVPacket *pkt, int packet_idx)
{
//...
print_val("size", pkt->size, unit_byte_str);
//...
}
本文讲述这个“size”值是怎样被计算出来的。
二、FFmpeg源码中计算WAV音频文件每个packet的size值的实现
(一)ff_pcm_default_packet_size函数
size值其实是通过源文件libavformat/pcm.c中的ff_pcm_default_packet_size函数计算出来的:
int ff_pcm_default_packet_size(AVCodecParameters *par)
{
int nb_samples, max_samples, bits_per_sample;
int64_t bitrate;
if (par->block_align <= 0)
return AVERROR(EINVAL);
max_samples = INT_MAX / par->block_align;
bits_per_sample = av_get_bits_per_sample(par->codec_id);
bitrate = par->bit_rate;
/* Don't trust the codecpar bitrate if we can calculate it ourselves */
if (bits_per_sample > 0 && par->sample_rate > 0 && par->ch_layout.nb_channels > 0)
if ((int64_t)par->sample_rate * par->ch_layout.nb_channels < INT64_MAX / bits_per_sample)
bitrate = bits_per_sample * (int64_t)par->sample_rate * par->ch_layout.nb_channels;
if (bitrate > 0) {
nb_samples = av_clip64(bitrate / 8 / PCM_DEMUX_TARGET_FPS / par->block_align, 1, max_samples);
nb_samples = 1 << av_log2(nb_samples);
} else {
/* Fallback to a size based method for a non-pcm codec with unknown bitrate */
nb_samples = av_clip(4096 / par->block_align, 1, max_samples);
}
return par->block_align * nb_samples;
}
从《音视频入门基础:WAV专题(4)——FFmpeg源码中获取WAV文件音频压缩编码格式、采样频率、声道数量、采样位数、码率的实现》中可以知道:
par->bit_rate为从WAV Header解码出来的音频码率,单位为bits/s。
par->bits_per_coded_sample为从WAV Header解码出来的音频采样位数。
par->channels为从WAV Header解码出来的声道数量。
par->sample_rate为从WAV Header解码出来的音频采样频率,单位为Hz。
par->block_align为从WAV Header解码出来的“区块对齐”,即每个采样点所需的字节数。
ff_pcm_default_packet_size函数中,首先计算出“最大采样”:
max_samples = INT_MAX / par->block_align;
将拿到的音频采样位数保存到变量bits_per_sample中;把拿到的音频码率(单位为bits/s)保存到变量bitrate中:
bits_per_sample = av_get_bits_per_sample(par->codec_id);
bitrate = par->bit_rate;
如果满足条件:从WAV Header中解码出来的音频采样位数、音频采样频率、声道数量都大于0,不使用从WAV Header中解码出来的音频码率,而是根据公式:音频码率 = 采样位数*采样频率*声道,计算:
/* Don't trust the codecpar bitrate if we can calculate it ourselves */
if (bits_per_sample > 0 && par->sample_rate > 0 && par->ch_layout.nb_channels > 0)
if ((int64_t)par->sample_rate * par->ch_layout.nb_channels < INT64_MAX / bits_per_sample)
bitrate = bits_per_sample * (int64_t)par->sample_rate * par->ch_layout.nb_channels;
宏PCM_DEMUX_TARGET_FPS定义在源文件libavformat/pcm.c中:
#define PCM_DEMUX_TARGET_FPS 10
关于av_clip、av_clip64用法可以参考:《FFmpeg源码:av_clip、av_clip64宏定义分析》、《FFmpeg源码:av_log2函数分析》。
nb_samples为一帧音频数据中采样的数量(次数)。
情况一:如果音频码率大于0,计算上述音频码率(单位为bits/s) ÷ 8 ÷ 10 ÷ “区块对齐”的结果,将该结果裁剪到1到“最大采样”的范围内,然后求该值是2的多少次幂,保存到变量nb_samples中;
情况二:如果音频码率不大于0,计算4096 ÷ “区块对齐”的结果,将该结果裁剪到1到“最大采样”的范围内,保存到变量nb_samples中:
if (bitrate > 0) {
nb_samples = av_clip64(bitrate / 8 / PCM_DEMUX_TARGET_FPS / par->block_align, 1, max_samples);
nb_samples = 1 << av_log2(nb_samples);
} else {
/* Fallback to a size based method for a non-pcm codec with unknown bitrate */
nb_samples = av_clip(4096 / par->block_align, 1, max_samples);
}
最后返回“区块对齐” × 一帧音频数据中采样的次数:
return par->block_align * nb_samples;
(二)wav->max_size
从《音视频入门基础:WAV专题(5)——FFmpeg源码中解码WAV Header的实现》中可以知道,FFmpeg源码通过wav_read_header函数解码WAV Header,该函数最后会调用set_max_size函数:
/* wav input */
static int wav_read_header(AVFormatContext *s)
{
//...
WAVDemuxContext *wav = s->priv_data;
set_max_size(st, wav);
return 0;
//...
}
set_max_size函数定义在源文件libavformat/wavdec.c中。可以看到该函数内部会调用ff_pcm_default_packet_size函数,把“区块对齐” × 一帧音频数据中采样的次数的结果赋值给变量max_size。如果max_size小于0,wav->max_size=4096,否则wav->max_size=“区块对齐” × 一帧音频数据中采样的次数:
static void set_max_size(AVStream *st, WAVDemuxContext *wav)
{
if (wav->max_size <= 0) {
int max_size = ff_pcm_default_packet_size(st->codecpar);
wav->max_size = max_size < 0 ? 4096 : max_size;
}
}
(三)AVPacket结构体得到size值
对于WAV音频文件,FFmpeg源码通过源文件libavformat/wavdec.c的wav_read_packet函数读取一个packet:
static int wav_read_packet(AVFormatContext *s, AVPacket *pkt)
{
//...
WAVDemuxContext *wav = s->priv_data;
//...
left = wav->data_end - avio_tell(s->pb);
//...
size = wav->max_size;
if (st->codecpar->block_align > 1) {
if (size < st->codecpar->block_align)
size = st->codecpar->block_align;
size = (size / st->codecpar->block_align) * st->codecpar->block_align;
}
size = FFMIN(size, left);
ret = av_get_packet(s->pb, pkt, size);
if (ret < 0)
return ret;
pkt->stream_index = 0;
return ret;
}
由《音视频入门基础:WAV专题(5)——FFmpeg源码中解码WAV Header的实现》中可以知道,wav->data_end为该WAV文件的总大小(单位为字节)。avio_tell(s->pb)为读取到该WAV音频文件的第几个字节了(关于avio_tell函数用法可以参考:《FFmpeg源码:avio_tell函数分析》)。所以wav_read_packet函数中,变量left的值等于该WAV音频文件中还剩下多少个字节没被读取:
left = wav->data_end - avio_tell(s->pb);
让变量size拿到wav->max_size的值,也就是“区块对齐” × 一帧音频数据中采样的次数的结果:
size = wav->max_size;
如果“区块对齐” × 一帧音频数据中采样的次数的结果小于“区块对齐”,size的值等于“区块对齐”;否则size的值等于“区块对齐” × 一帧音频数据中采样的次数的结果:
if (st->codecpar->block_align > 1) {
if (size < st->codecpar->block_align)
size = st->codecpar->block_align;
size = (size / st->codecpar->block_align) * st->codecpar->block_align;
}
让size的值取上述得到的size值和“该WAV音频文件中还剩下多少个字节没被读取”中的最小值,这是因为读取WAV音频文件到最后,剩下还未被读取的数据的字节数是不满一个packet的大小的:
size = FFMIN(size, left);
最后通过av_get_packet函数(关于该函数用法可以参考:《FFmpeg源码:append_packet_chunked、av_get_packet函数分析》),增加该packet大小至size个字节,也就是让pkt->size增至size字节,从而设置AVPacket结构体中的size成员变量:
ret = av_get_packet(s->pb, pkt, size);
三、总结
WAV音频文件每个packet的size值一般为:“区块对齐(每个采样点所需的字节数)” × 一帧音频数据中采样的次数。如果读取到WAV音频文件的最后,size值为剩下的还未被读取的不满一个packet大小的字节数。