FFmpeg音频解码后输出的为PCM数据,PCM中的声音数据没有被压缩。
FFmpeg中音视频数据基本上都有Packed和Planar两种存储方式,对于双声道音频来说,Packed方式为两个声道的数据交错存储,交织在一起;Planar方式为两个声道分开存储,也就是平铺分开。假设一个L/R为一个采样点的话(一个采样点可能是8位16位32位等),可以这么表示:
Packed: L R L R L R L R Planar: L L L L R R R R
FFmpeg音频解码后的数据是存放在AVFrame frame结构中的,如果是Packed格式的话,所有的音频数据都放在frame.data[0]结构中;如果是Planar格式的话,不同声道的数据分别放在frame.data[0]和frame.data[1]中。
下面为FFmpeg音频采样格式,所有的Planar格式后面都有字母P标识。
enum AVSampleFormat {
AV_SAMPLE_FMT_NONE = -1,
AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
AV_SAMPLE_FMT_S16, ///< signed 16 bits
AV_SAMPLE_FMT_S32, ///< signed 32 bits
AV_SAMPLE_FMT_FLT, ///< float
AV_SAMPLE_FMT_DBL, ///< double
AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
AV_SAMPLE_FMT_FLTP, ///< float, planar
AV_SAMPLE_FMT_DBLP, ///< double, planar
AV_SAMPLE_FMT_S64, ///< signed 64 bits
AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
};
不同的格式的输入音频解码后输出的音频采样格式不是固定的,最常见的音频格式有AAC和MP3两种,我测试中,其中AAC解码输出的数据为浮点型的 AV_SAMPLE_FMT_FLTP 格式,MP3解码输出的数据为 AV_SAMPLE_FMT_S16P 格式(使用的mp3文件为16位深)。具体采样格式可以查看解码后的AVFrame中的format成员或解码器的AVCodecContext中的sample_fmt成员。
这里AAC和MP3音频解码后的数据都是Planar模式的,两个声道的声音数据分别存在frame.data[0]和frame.data[1]中,多声道音频可能还会使用data[2] data[3]等。需要注意的是: 我们刚分析的Planar和Packed模式是ffmpeg内部存储模式,我们实际使用的音频文件都是LRLR左右声道交替存储的,设想如果音频文件3MB大小的话,不可能前面1.5MB存左声道,后面1.5MB存右声道。
Planar或者Packed模式直接影响到保存文件时写文件的操作,所以操作数据的时候一定要先检测音频采样格式。下面以Planar格式来演示如何保存音频文件。
保存PCM格式数据到文件
// 前面代码读音频文件,初始化FFmpeg并打开了AVCodecContext
// 下面代码进行解码和保存文件
bool AudioDecoder::readFrameProc()
{
FILE *fd = fopen("out.pcm", "wb");
AVPacket packet;
//av_init_packet(&packet);
AVFrame *frame = av_frame_alloc();
// 读取一个帧packet的音频数据
while (int num = av_read_frame(mFormatCtx, &packet) >= 0) {
// 解码(发送一个packet,获取到的frame就是解码后的数据了),这是FFmpeg 3的新解码函数
avcodec_send_packet(mCodecCtx, &packet);
int ret = avcodec_receive_frame(mCodecCtx, frame);
if (!ret) {
// 获取一个采样点字节数,比如16位采样点值为2字节
int data_size = av_get_bytes_per_sample(mCodecCtx->sample_fmt);
// frame->nb_samples为这个frame中一个声道的采样点的个数
for (int i = 0; i < frame->nb_samples; i++)
for (int ch = 0; ch < mCodecCtx->channels; ch++)
fwrite(frame->data[ch] + data_size*i, 1, data_size, fd);
}
av_packet_unref(&packet);
}
av_frame_free(&frame);
fclose(fd);
return false;
}
上面这段代码frame为解码后的数据,因为是Planar模式的数据,所以写文件的时候,存储每采样点的时候两个声道LRLRLR这样交错写入文件,相当于把AV_SAMPLE_FMT_S16P 采样格式保存为 不带字母P的AV_SAMPLE_FMT_S16 采样格式,。
举个例子,假如这个frame中有20个采样点(nb_samples=20),每个采样点为2字节(16位深,每个声道的一个采样点2字节)。左声道数据为data[0][40]数组,右声道数据为data[1][40]数组。写文件的时候依次写入 data[0][0],data[0][1] – data[1][0],data[1][1] – data[0][2],data[0][3] – data[1][2]-data[1][3]。
播放PCM音频文件
保存的PCM文件可以使用ffplay指定参数进行播放:
ffplay -f 格式名 -ac 声道数 -ar 采样率 文件名
本文没有讨论大端存储还是小端存储的问题,因为我们通用的PC机默认都是小端存储的。比如我使用的MP3解码为AV_SAMPLE_FMT_S16P格式的,对应解码器格式名为s16le
,ffplay播放指令为:
ffplay -f s16le -ac 2 -ar 44100 out.pcm
AAC解码为AV_SAMPLE_FMT_FLTP格式,对应格式名为f32le
。