一. 音频帧概率详解:
- 概念
1)采样率(Sample Rate):每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。
一般音乐CD的采样率是 44100Hz,所以视频编码中的音频采样率保持在这个级别就完全足够了,通常视频转换器也将这个采样率作为默认设置。
2)帧率(Frame rate):是用于测量显示帧数的量度。所谓的测量单位为每秒显示帧数(Frames per Second,简称:FPS)或"赫兹"(Hz)。
3)码率(Bit Rate):指视频或音频文件在单位时间内使用的数据流量,该参数的单位通常是Kbps,也就是千比特每秒。
通常2000kbps~3000kbps就已经足以将画质效果表现到极致了。码率参数与视频文件最终体积大小有直接性的关系
4)正常人听觉的频率范围大约在20Hz~20kHz之间,根据奈奎斯特采样理论,为了保证声音不失真,采样频率应该在40kHz左右。
常用的音频采样频率有8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz等,如果采用更高的采样频率,还可以达到DVD的音质
对采样率为44.1kHz的AAC音频进行解码时,一帧的解码时间须控制在23.22毫秒内。
(一个AAC原始帧包含一段时间内1024个采样及相关数据)
- 分析:
- AAC
音频帧的播放时间=一个AAC帧对应的采样样本的个数/采样频率(单位为s)
一帧 1024个 sample。采样率 Samplerate 44.1KHz,每秒44100个sample, 所以根据公式 音频帧的播放时间=一个AAC帧对应的采样样本的个数/采样频率
当前AAC一帧的播放时间是= 1024*1000/44100= 22.32ms (单位为ms)
------------------------------------------------------------------------------------------------------------------------------
1、就是改变音频的采样率、sample format、 声道数等参数,使之按照我们期望的参数输出。
2、当然是原有的音频参数不满足我们的需求,比如在FFmpeg解码音频的时候,
不同的音源有不同的格式,采样率等,在解码后的数据中的这些参数也会不一致
(最新FFmpeg 解码音频后,音频格式为AV SAMPLE FMT FLTP,这个参数应该是一致的),
如果我们接下来需要使用解码后的音频教据做其他操作,而这些参数的不一致导致会有很多额外工作,
此时直接对其进行重采样,获取我们制定的音频参数,这样就会方便很多。
再比如在将音频进行 SDL 播放时候,因为当前的 SDL2.0 不支持planar格式,也不支持浮点型的,
而最新的FFMPEG 16年会将音频解码为AV_SAMPLE_FMT_FLTP格式,因此此时就需要我们对其重采样,使之可以在SDL2 上进行播
1.3 可调节的参数
通过重采样,我们可以对:
· sample rate (采样率)
· sample format (采样格式)
· channel layout(通道布局,可以通过此参数获取声道数)
2 对应参数解析
分片(plane)和打包(packed)
以双声道为例,带P(plane)的数据格式在存储时,其左声道和右声道的数据是分开存储的,
左声道的数据存储在data[0],右声道的数据存储在data[1],每个声道的所占用的字节数为linesize[0]和linesize[1];
不带P(packed)的音频数据在存储时,是按照LRLRL...的格式交替存储在data[0]中,linesize[0]表示总的数据量。
2.4 声道分布(channel layout)
声道分布在FFmpeg\libavuti\channel_layout.h 中有定义,一般来说用的比较多的是
AV_CH_LAYOUT_STEREO(双声道)和AV_CH_LAYOUT SURROUND(三声道),这两者的定义如下:
#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFTI AV_CH_FRONT_RIGHT)
#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER)
2.5 音频帧的数据量计算
一帧音频的数据量(字节) = channel 数 * nb_samples 样本数 * 每个样本占用的字节数
如果该音频帧是FLTP格式的PCM数据,包含1024个样本,双声道,那么该音频帧包含的音频数据量是2*1024*4=8192字节
2.6 音频播放时间计算
以采样率44100Hz来计算,每秒44100个sample,而正常一帧为1024个 sample,
可知每帧播放时间/1024 = 1000ms/44100,得到每帧播放时间=1024*1000/44100=23.2ms(更精确的是23.21995464852608)。
一帧播放时间(毫秒) = nb_samples * 1000 / 采样率
cpp
int GetFormatFromSampleFormat(const char **format, enum AVSampleFormat sampleFormat)
{
struct SampleFormatEntry
{
enum AVSampleFormat SampleFormat;
const char *Format_be;
const char *Format_le;
} SampleFormatEntries[] = {
{AV_SAMPLE_FMT_U8, "u8", "u8"},
{AV_SAMPLE_FMT_S16, "s16be", "s16le"},
{AV_SAMPLE_FMT_S32, "s32be", "s32le"},
{AV_SAMPLE_FMT_FLT, "f32be", "f32le"},
{AV_SAMPLE_FMT_DBL, "f64be", "f64le"},
};
*format = NULL;
for (int i = 0; i < FF_ARRAY_ELEMS(SampleFormatEntries); i++) // FF_ARRAY_ELEMS 数组元素个数
{
struct SampleFormatEntry *entry = &SampleFormatEntries[i];
if (sampleFormat == entry->SampleFormat)
{
/**
* 这段代码定义了一个宏AV_NE(be, le),用来根据系统的字节序编码来选择参数be或le。
* 如果系统是大端序(big endian),则AV_NE(be, le)会返回be;
* 如果系统是小端序(little endian),则会返回le。
* 这个宏可以在跨平台开发中用来处理不同字节序下的数据处理问题。
*/
*format = AV_NE(entry->Format_be, entry->Format_le); // AV_NE -- 返回适配的大小端
return 0;
}
}
// av_get_sample_fmt_name 是一个函数,用于获取音频采样格式的名称。
// 该函数接受一个音频采样格式的枚举值作为参数,并返回对应的格式名称作为字符串。
// 这样可以方便地将枚举值转换为可读性更好的格式名称,用于输出日志、调试信息或用户界面上显示音频采样格式。
av_log(NULL, AV_LOG_ERROR, "音频采样格式: %s 无法作为输出格式输出\n", av_get_sample_fmt_name(sampleFormat));
return AVERROR(EINVAL);
}
cpp
// 填充指定长度、声道数的正弦波样本数据到目标缓冲区中 --- 交错模式
void FillAudioSampleData(double *destination /*目标缓冲区*/, int samples /*样本数*/, int channels /*声道数*/, int SampleRate /*采样率*/, double *time /*时间变量*/)
{
// DeltaTime 每帧之间的时间差
double DeltaTime = 1.0 / SampleRate;
double *destinationPointer = destination;
const double SineWaveCoefficient = 2 * M_PI * 440.0; // 正弦波系数
for (int i = 0; i < samples; i++)
{
*destinationPointer = sin(SineWaveCoefficient * (*time));
for (int j = 1; j < channels; j++)
destinationPointer[j] = destinationPointer[0];
destinationPointer += channels;
*time += DeltaTime;
}
}
cpp
int ResamplePCM(const char *outFileName)
{
int ret = -1; // 记录 FFmpeg 调用返回值
// 输出文件
FILE *outFile = fopen(outFileName, "wb");
if (!outFile)
{
av_log(NULL, AV_LOG_ERROR, "[%s] open file: %s error -- line:%d\n", __FUNCTION__, outFileName, __LINE__);
goto _end;
}
size_t DestinationBufSize;
// 输入参数
int64_t SourceChannelLayout = AV_CH_LAYOUT_STEREO; // 立体声声道布局的宏
int SourceSampleRate = 48000; // 采样率
enum AVSampleFormat SourceSampleFormat = AV_SAMPLE_FMT_DBL; // 音频样本的数据类型 -- 双精度浮点采样格式
int SourceChannels = 0; // 声道数
uint8_t **SourceData = NULL;
int SourceLinesize;
size_t SourceSampleSize = 1024; // 源样本数量 - 一帧包含的采样点数量 - 通常一个AAC帧包含1024个采样点
// 输出参数
int64_t DestinationChannelLayout = AV_CH_LAYOUT_STEREO; // 立体声声道布局的宏
int DestinationSampleRate = 44100; // 采样率
enum AVSampleFormat DestinationSampleFormat = AV_SAMPLE_FMT_S16; // 代表有符号16位整数的音频采样格式,通常用于表示音频数据中每个采样点的数据类型。
int DestinationChannels = 0; // 声道数
uint8_t **DestinationData = NULL;
int DestinationLinesize;
size_t DestinationSampleSize; // 目的样本数量
size_t MaxDestinationSampleSize; // 目的最大样本数量
// 重采样实例
struct SwrContext *swrCtx;
// 创建重采样器
swrCtx = swr_alloc();
if (!swrCtx)
{
av_log(NULL, AV_LOG_ERROR, "[%s] swr_alloc error -- line:%d\n", __FUNCTION__, __LINE__);
ret = AVERROR(ENOMEM);
goto _end;
}
// 设置采样参数
// 设置音频重采样上下文的输入声道布局选项值为 SourceChannelLayout,这里的SourceChannelLayout应该是一个代表声道布局的整数值,这样设置输入声道布局的选项可以告诉音频重采样器如何处理输入数据的声道布局。
av_opt_set_int(swrCtx /*要设置选项的对象的指针*/, "in_channel_layout" /*选项名称字符串*/, SourceChannelLayout /*待设置的整数值*/, 0 /*选项搜索标志,通常可以设为0*/);
// 这行代码设置音频重采样上下文的输入采样率选项值为 SourceSampleRate, 表示输入数据的采样率。这个选项的设置告诉音频重采样器输入音频数据的采样率是多少。
av_opt_set_int(swrCtx, "in_sample_rate", SourceSampleRate, 0);
// 设置音频重采样上下文的输入采样格式选项值为 SourceSampleFormat,这里的SourceSampleFormat应该是一个代表采样格式的枚举值,通过设置输入采样格式的选项,可以告诉音频重采样器输入音频数据的采样格式是什么。
av_opt_set_sample_fmt(swrCtx, "in_sample_fmt", SourceSampleFormat, 0);
// 输出参数
av_opt_set_int(swrCtx, "out_channel_layout", DestinationChannelLayout, 0);
av_opt_set_int(swrCtx, "out_sample_rate", DestinationSampleRate, 0);
av_opt_set_sample_fmt(swrCtx, "out_sample_fmt", DestinationSampleFormat, 0);
// 初始化重采样
ret = swr_init(swrCtx);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] Failed to initialize the resampling context -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
// 计算输入源的通道数量
SourceChannels = av_get_channel_layout_nb_channels(SourceChannelLayout);
av_log(NULL, AV_LOG_INFO, "[%s] SourceChannels:%d -- line:%d\n", __FUNCTION__, SourceChannels, __LINE__);
// 给输入源分配内存空间
/* int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels, int nb_samples, enum AVSampleFormat sample_fmt, int align)
* 分配一个数据指针数组,为nb_samples样本分配样本缓冲区,并相应地填充数据指针和linesize。这与av_samples_alloc()相同,但也会分配数据指针数组。*/
ret = av_samples_alloc_array_and_samples(&SourceData, &SourceLinesize, SourceChannels, SourceSampleSize, SourceSampleFormat, 0);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] Allocate memory space for the input source -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
// 计算输出采样数量
// int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd)
// av_rescale_rnd - 使用指定的舍入值重新缩放64位整数。该操作在数学上等同于a * b / c,但直接写入可能溢出,并且不支持不同的舍入方法。如果结果不可表示,则返回INT64_MIN。
// SourceSampleSize * DestinationSampleRate / SourceSampleRate
MaxDestinationSampleSize = DestinationSampleSize = av_rescale_rnd(SourceSampleSize, DestinationSampleRate, SourceSampleRate, AV_ROUND_UP);
av_log(NULL, AV_LOG_INFO, "[%s] MaxDestinationSampleSize = DestinationSampleSize = %ld -- line:%d\n", __FUNCTION__, MaxDestinationSampleSize, __LINE__);
// av_get_channel_layout_nb_channels - 用于获取指定声道布局的声道数量。
DestinationChannels = av_get_channel_layout_nb_channels(DestinationChannelLayout);
av_log(NULL, AV_LOG_INFO, "[%s] DestinationChannels:%d -- line:%d\n", __FUNCTION__, DestinationChannels, __LINE__);
// 分配输出缓存内存
ret = av_samples_alloc_array_and_samples(&DestinationData, &DestinationLinesize, DestinationChannels, DestinationSampleSize, DestinationSampleFormat, 0);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] Allocate output cache memory error! -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
double time = 0;
do
{
// 生成输入源
FillAudioSampleData((double *)SourceData[0], SourceSampleSize, SourceChannels, SourceSampleRate, &time);
// 计算目标样本数量
int64_t delay = swr_get_delay(swrCtx, SourceSampleRate);
// 将延迟值和源音频大小转换为目标音频大小
DestinationSampleSize = av_rescale_rnd(delay + SourceSampleSize, DestinationSampleRate, SourceSampleRate, AV_ROUND_UP);
if (DestinationSampleSize > MaxDestinationSampleSize)
{
av_freep(&DestinationData[0]);
// 重新分配
// 为nb_samples样本分配一个样本缓冲区,并相应地填充数据指针和linesize。分配的样本缓冲区可以使用av_freep(&audio_data[0])释放,分配的数据将初始化为静音。
ret = av_samples_alloc(DestinationData /*数组,以填充每个通道的指针*/,
&DestinationLinesize /*音频缓冲区的对齐大小,可以为NULL*/,
DestinationChannels /*音频通道数*/,
DestinationSampleSize /*每个通道的采样数*/,
DestinationSampleFormat /*示例格式*/,
1 /*缓冲区大小对齐方式(0 =默认,1 =没有对齐)*/);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] Reoutput allocated memory error! -- line:%d\n", __FUNCTION__, __LINE__);
break;
}
MaxDestinationSampleSize = DestinationSampleSize;
}
/** 转换音频。 int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in, int in_count)
* In和in_count可以设置为0,以在结束时将最后几个样本清除。
* 如果提供的输入空间大于输出空间,则输入将被缓冲。
* 可以使用swr_get_out_samples()来获取给定输入样本数量所需输出样本数量的上界,从而避免这种缓冲。
* 转换将直接运行,而不需要尽可能地复制。
**/
int SampleSize /*每个通道输出的样本数*/ = swr_convert(swrCtx, // 已分配的Swr上下文,并设置了参数
DestinationData, // 输出缓冲,打包音频时只需要设置第一个
DestinationSampleSize, // 每个通道中可供输出样本的空间量
(const uint8_t **)SourceData, // 输入缓冲器,在打包音频的情况下,只需要设置第一个
SourceSampleSize // 一个通道中可用的输入样本数
);
if (SampleSize < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] Reoutput allocated memory error! -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
// 获取给定音频参数所需的缓冲区大小。
DestinationBufSize = av_samples_get_buffer_size(&DestinationLinesize, DestinationChannels, SampleSize, DestinationSampleFormat, 1);
if (DestinationBufSize < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] av samples get buffer size error! -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
av_log(NULL, AV_LOG_DEBUG, "[%s] time:%f, in:%ld out:%d -- line:%d\n", __FUNCTION__, time, SourceSampleSize, SampleSize, __LINE__);
fwrite(DestinationData[0], 1, DestinationBufSize, outFile);
} while (time < 3);
int FlushSampleSize = swr_convert(swrCtx, DestinationData, DestinationSampleSize, NULL, 0);
if (FlushSampleSize < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] allocated memory error! -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
DestinationBufSize = av_samples_get_buffer_size(&DestinationLinesize, DestinationChannels, FlushSampleSize, DestinationSampleFormat, 1);
if (DestinationBufSize < 0)
{
av_log(NULL, AV_LOG_ERROR, "[%s] av samples get buffer size error! -- line:%d\n", __FUNCTION__, __LINE__);
goto _end;
}
av_log(NULL, AV_LOG_INFO, "[%s] flush in:%d out:%d -- line:%d\n ", __FUNCTION__, 0, ret, __LINE__);
fwrite(DestinationData[0], 1, DestinationBufSize, outFile);
const char *FormatString;
if ((ret = GetFormatFromSampleFormat(&FormatString, DestinationSampleFormat)) < 0)
{
goto _end;
}
av_log(NULL, AV_LOG_INFO, "ffplay -f %s -channel_layout %ld -channels %d -ar %d %s\n", FormatString, DestinationChannelLayout, DestinationChannels, DestinationSampleRate, outFileName);
_end:
if (outFile)
{
fclose(outFile);
}
if (SourceData)
{
av_freep(&SourceData[0]);
}
av_freep(&SourceData);
if (DestinationData)
{
av_freep(&DestinationData[0]);
}
av_freep(&DestinationData);
swr_free(&swrCtx);
return ret;
}