1、总流程
- 创建解析器、解码器、AVPacket和AVFrame
- 打开文件,将mp3数据读入缓冲区
- 解析mp3数据(在 main 函数中完成)
- 解码,并将解码后的pcm数据写入文件(在 my_audio_decode 函数中完成
2、解析流程
mp3文件可能比较大,一次性读取会浪费比较多的内存,采用边读边解析办法。
如下图所示,红色表示buf缓冲区,首先从mp3文件里读取数据填满buf,然后开始解析,buf_index负责标记解析结束位置,buf_size负责记录buf里还剩多少数据未解析,如果buf_size小于4096,则将剩余未解析的数据移动到buf起始位置,然后再次从文件里读数据填满buf,同时buf_index也移动到buf起始位置,以此循环直到文件读取结束。
- 代码实现如下
buf_size = fread(buf, 1, AUDIO_BUF_SIZE, fl_in); // 读mp3文件
while (buf_size > 0)
{
ret = av_parser_parse2(parser, ctx, &pkt->data, &pkt->size, buf_index, buf_size,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) { exit(1); } // parser失败,直接退出程序
buf_index += ret; // 跳过已经解析的数据
buf_size -= ret; // 缓冲区还未解析的mp3数据大小
if (pkt->size)
my_audio_decode(ctx, pkt, frame, fl_out); // 解码并将解码后的pcm数据写入文件
if (buf_size < 4096) // 缓冲区未解析的音频数据小于4096,再从mp3文件中读取一点数据放入缓冲区
{
memmove(buf, buf_index, buf_size); // 把还未解析的数据移动到buf的起始位置
buf_index = buf; // 当前解析结束位置也移动到buf起始位置
len = fread(buf_index + buf_size, 1, AUDIO_BUF_SIZE - buf_size, fl_in);
if (len > 0) buf_size += len;
}
}
3、解码流程
解码流程相对简单,这里单独用一个函数封装。解析完后直接从 AVPacket 里面将数据解码得到解码数据即可,然后写入文件。音频文件和视频文件的写法不同,注意做区分,代码实现如下:
// 解码
static void my_decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, FILE *fl_out, AVCodecID codec_id)
{
int size;
int ret = avcodec_send_packet(ctx, pkt); // 发送packet到解码线程,不占用CPU资源
if (ret < 0) { return; } // 需要注意 AVERROR(EAGAIN) 错误处理
while (ret >= 0)
{
ret = avcodec_receive_frame(ctx, frame); //从解码线程中获取解码接口,不占用CPU资源
if (ret != 0) break;
size = av_get_bytes_per_sample(ctx->sample_fmt); // 获取每个样本的字节数
if (size < 0) { exit(1); } // 异常!大小计算失败
// 将解码后的yuv或pcm数据写入文件
if (codec_id == AV_CODEC_ID_H264) // h264视频
{
print_video_format(frame); // 打印视频基本参数
for(int j=0; j<frame->height; j++) // Y
fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, fl_out);
for(int j=0; j<frame->height/2; j++) // U
fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, fl_out);
for(int j=0; j<frame->height/2; j++) // V
fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, fl_out);
}
else // mp3或aac音频
{
print_audio_format(frame); // 打印音频基本参数
for (int i = 0; i < frame->nb_samples; i++) // 采样率
{
for (int channel = 0; channel < ctx->channels; ++channel) // 声道数
{
fwrite(frame->data[channel] + size*i, 1, size, fl_out); // 将pcm数据写入文件
}
}
}
}
}
4、完整代码
以下代码再Qt5.14.0中编译测试OK,运行目录需要放置一个believe.mp3文件,程序执行完成后会在运行目录下生成一个believe.pcm文件,使用以下命令即可测试转换是否成功:
音频:ffplay -ar 48000 -ac 2 -f f32le believe.pcm
视频:ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 out.yuv
#ifdef __cplusplus
extern "C"
{
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
#ifdef __cplusplus
}
#endif
// 缓冲区大小
#define BUF_SIZE 20480
// 解码
static void my_decode(AVCodecContext *ctx, AVPacket *pkt, AVFrame *frame, FILE *fl_out, AVCodecID codec_id)
{
int size;
int ret = avcodec_send_packet(ctx, pkt); // 发送packet到解码线程,不占用CPU资源
if (ret < 0) { return; } // 需要注意 AVERROR(EAGAIN) 错误处理
while (ret >= 0)
{
ret = avcodec_receive_frame(ctx, frame); //从解码线程中获取解码接口,不占用CPU资源
if (ret != 0) break;
size = av_get_bytes_per_sample(ctx->sample_fmt); // 获取每个样本的字节数
if (size < 0) { exit(1); } // 异常!大小计算失败
// 将解码后的yuv或pcm数据写入文件
if (codec_id == AV_CODEC_ID_H264) // h264视频
{
for(int j=0; j<frame->height; j++) // Y
fwrite(frame->data[0] + j * frame->linesize[0], 1, frame->width, fl_out);
for(int j=0; j<frame->height/2; j++) // U
fwrite(frame->data[1] + j * frame->linesize[1], 1, frame->width/2, fl_out);
for(int j=0; j<frame->height/2; j++) // V
fwrite(frame->data[2] + j * frame->linesize[2], 1, frame->width/2, fl_out);
}
else // mp3或aac音频
{
for (int i = 0; i < frame->nb_samples; i++) // 采样率
{
for (int channel = 0; channel < ctx->channels; ++channel) // 声道数
{
fwrite(frame->data[channel] + size*i, 1, size, fl_out); // 将pcm数据写入文件
}
}
}
}
}
// YUV视频播放范例:ffplay -pixel_format yuv420p -video_size 766x322 -framerate 25 out.yuv
// pcm音频播放示例:ffplay -ar 48000 -ac 2 -f f32le believe.pcm
int main()
{
const char *outfilename = "out.pcm"; // 要输出的yuv文件路径
const char *infilename = "believe.mp3"; // h264文件路径,200kbps 766x322 10s
FILE *fl_in = NULL;
FILE *fl_out = NULL;
const AVCodec *codec = NULL; // 解码器
AVCodecContext *ctx = NULL; // 解码器上下文
AVCodecParserContext *parser = NULL; // 解析器,解码前需要先解析
AVPacket *pkt = av_packet_alloc(); // 接封装后的帧
AVFrame *frame = av_frame_alloc(); // 解码后的帧
uint8_t buf[BUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE]; // yuv帧数据缓冲区
size_t buf_size = 0; // 缓冲区未解析的数据大小
uint8_t *buf_index = buf; // 当前解析位置,初始化为buf起始位置
enum AVCodecID codec_id = AV_CODEC_ID_MP3; // 解码器ID
int len = 0; // 从文件中读到的数据大小
int ret = 0;
// ===== 根据文件名后缀判断是视频还是音频 =====
if (strstr(infilename, "aac") != NULL){ // aac音频裸流
codec_id = AV_CODEC_ID_AAC;
} else if (strstr(infilename, "mp3") != NULL) { // mp3音频裸流
codec_id = AV_CODEC_ID_MP3;
} else if (strstr(infilename, "h264") != NULL) { // h264视频裸流
codec_id = AV_CODEC_ID_H264;
} else {
return 1; // 不支持的格式
}
// ================== 1、初始化 ==================
if (!frame || !pkt){ exit(1); } // 帧空间分配失败
codec = avcodec_find_decoder(codec_id); // 查找解码器
if (!codec) { exit(1); }
parser = av_parser_init(codec->id); // 获取裸流的解析器
if (!parser) { exit(1); }
ctx = avcodec_alloc_context3(codec); // 分配codec上下文
if (!ctx) { exit(1); }
ret = avcodec_open2(ctx, codec, NULL); // 将解码器和解码器上下文进行关联
if (ret < 0) { exit(1); }
fl_in = fopen(infilename, "rb"); // 打开输入文件
fl_out = fopen(outfilename, "wb"); // 打开输出文件
if (!fl_in || !fl_out) {
av_free(ctx);
exit(1);
}
// ================== 2、开始解码 ==================
buf_size = fread(buf, 1, BUF_SIZE, fl_in); // 读mp3文件
while (buf_size > 0)
{
ret = av_parser_parse2(parser, ctx, &pkt->data, &pkt->size, buf_index, buf_size,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0) { exit(1); } // parser失败,直接退出程序
buf_index += ret; // 跳过已经解析的数据
buf_size -= ret; // 缓冲区还未解析的mp3数据大小
if (pkt->size)
my_decode(ctx, pkt, frame, fl_out, codec_id); // 解码并将解码后的pcm数据写入文件
if (buf_size < 4096) // 缓冲区音频数据小于4096,再从mp3文件中读取一点数据放入缓冲区
{
memmove(buf, buf_index, buf_size); // 把还未解析的数据移动到buf的起始位置
buf_index = buf; // 当前解析位置也移动到buf起始位置
len = fread(buf_index + buf_size, 1, BUF_SIZE - buf_size, fl_in);
if (len > 0) buf_size += len;
}
}
// ================== 3、解码结束 ==================
pkt->data = NULL; // 进入drain mode
pkt->size = 0;
my_decode(ctx, pkt, frame, fl_out, codec_id);
fclose(fl_out); // 关闭文件
fclose(fl_in);
// 释放空间
avcodec_free_context(&ctx);
av_parser_close(parser);
av_frame_free(&frame);
av_packet_free(&pkt);
printf("\n转换成功\n");
return 0;
}
说明:代码中通过文件后缀名判断文件是音频还是视频,这种方式不太可取,后面再考虑优化一下。