ffmpeg项目巨大,本文针对ffmpeg学习(3)编码、解码的流程介绍中编码部分,完成以下内容:
(1) 实现对音频采样数据pcm进行MP3/AAC编码、保存; (2) 音频格式转码,保存封装
关于解码的流程主要流程如下
流程图中使用的函数,可以通过其名称了解其用处,网上参考较多,也可以直接看api文档。
其中关于结构体AVFrame和AVPackt的介绍参看文章:
ffmpeg学习 结构体分析AVFrameffmpeg学习 结构体分析AVPackffmpeg学习 音频采样数据PCM
和视频编码类似,对于音频pcm数据,根据编码器需送入固定数量的采样数据,编码后保存。由于编码的裸流音频数据文件可能无法播放(AAC不能直接播放),需要进行额外处理或直接封装,关于文件封装格式,参考博客ffmpeg学习(1)背景知识-基础知识ffmpeg学习(4)协议解析、封装解析
MP3编码的每一个帧数据,由帧头、附加信息和声音数据,播放器都能从任何帧开始进行解码播放
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
#include "libavcodec/avcodec.h"
#include "libavutil/opt.h"
#ifdef __cplusplus
}
#endif
static void encode(AVCodecContext *enc_ctx, const AVFrame *frame, AVPacket *pkt, FILE *outfile)
{
int ret;
/* send the frame to the encoder */
//if(frame)
// printf("Send frame %lld\n", frame->pts);
ret = avcodec_send_frame(enc_ctx, frame);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");
exit(1);
}
while(ret >= 0) {
ret = avcodec_receive_packet(enc_ctx, pkt);
if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
}
else if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");
exit(1);
}
//write to file
printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);
fwrite(pkt->data, 1, pkt->size, outfile);
av_packet_unref(pkt);
}
}
int main()
{
// 输入音频文件信息
int in_sample_rates = 8000;
uint64_t in_sample_layout = AV_CH_LAYOUT_STEREO;
AVSampleFormat in_sample_format = AV_SAMPLE_FMT_S16;
//const char *in_file_name = "../files/Titanic_48000_s16_stero.pcm";
const char *in_file_name = "../files/Titanic_8000_s16_stero.pcm";
// 输出编码流文件信息(和输入相同)
int out_sample_rates = 8000;
uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
//AVCodecID codec_id = AV_CODEC_ID_AAC;
//AVSampleFormat out_sample_format = AV_SAMPLE_FMT_FLTP; // aac 固定使用FLTP
//const char *out_file_name = "Titanic_out.aac";
AVCodecID codec_id = AV_CODEC_ID_MP3;
AVSampleFormat out_sample_format = AV_SAMPLE_FMT_S16P; // mp3 固定使用S16p
const char *out_file_name = "Titanic_out.mp3";
// 输入输出文件
FILE *in_file, *out_file;
in_file = fopen(in_file_name, "rb");
if(!in_file) {
fprintf(stderr, "Can not open file %s\n", in_file_name);
return 0;
}
out_file = fopen(out_file_name, "wb");
if(!out_file) {
fprintf(stderr, "Can not open file %s\n", out_file_name);
return 0;
}
// video encoder
const AVCodec *encodec = avcodec_find_encoder(codec_id);
if(!encodec) {
av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));
return 0;
}
// video encoder contex
AVCodecContext *encoder_ctx = avcodec_alloc_context3(encodec);
if(!encoder_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");
return 0;
}
// encoder parameters
encoder_ctx->sample_rate = out_sample_rates;
encoder_ctx->sample_fmt = out_sample_format;
encoder_ctx->channel_layout = out_ch_layout;
// open condec
int ret = avcodec_open2(encoder_ctx, encodec, NULL);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");
avcodec_free_context(&encoder_ctx);
return 0;
}
// allocate variables
AVFrame *frame = av_frame_alloc();
//av_samples_alloc(frame->data, frame->linesize,
// encoder_ctx->channels, encoder_ctx->frame_size, encoder_ctx->sample_fmt, 1);
frame->channel_layout = encoder_ctx->channel_layout;
frame->nb_samples = encoder_ctx->frame_size;
frame->format = encoder_ctx->sample_fmt;
av_frame_get_buffer(frame, 1);
AVPacket *pkt = av_packet_alloc();
// start encoder
int64_t frame_cnt = 1;
while(!feof(in_file)) {
for(int i = 0; i < frame->nb_samples; i++) {
int16_t intens[2];
fread(intens, sizeof(int16_t) * 2, 1, in_file);
if(codec_id == AV_CODEC_ID_AAC) {
// s16 => fltp aac
*((float*)(frame->data[0]) + i) = intens[0] / 32768.;
*((float*)(frame->data[1]) + i) = intens[1] / 32768.;
}
else if(codec_id == AV_CODEC_ID_MP3) {
// s16 => s16p mp3
*((int16_t*)(frame->data[0]) + i) = intens[0];
*((int16_t*)(frame->data[1]) + i) = intens[1];
}
}
frame->pts = frame_cnt++;
encode(encoder_ctx, frame, pkt, out_file);
}
// flush
encode(encoder_ctx, NULL, pkt, out_file);
fclose(in_file);
fclose(out_file);
avcodec_free_context(&encoder_ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
}
以上示例将输入Titanic_8000_s16_stero.pcm 进行MP3编码并保存为裸流文件,可以直接进行播放。使用MediaInfo查看信息如下。
调整代码,使用AAC编码,运行上述代码运行,会提示一些警告信息,如
[aac @ 0000016010a81280] Too many bits 16384.000000 > 12288 per frame requested, clamping to max
[aac @ 0000016010a81280] Qavg: 53900.766
需要在编码器参数上额外设置编码码率,
if(codec_id == AV_CODEC_ID_AAC)
encoder_ctx->bit_rate = 64000; // 1024/1152 * sizeof(float) * chns * 8bit= 65536/71728
前面提到,AAC编码保存的裸流文件,不能直接播放。使用MediaInfo查看信息如下
不能直接看到这个文件的任何相关信息,将该文件导入QQ音乐中提示“添加文件出错,强检查文件”。这是因为保存的aac文件首先非封装文件,其次裸流文件中每一帧不包含编码信息,播放器解码器不能正确解析并播放。为每一帧AAC数据添加的编码信息专业术语叫adts头。
根据编码信息,生成adts头数据的函数
static char* adts_gen(const int packetLen)
{
static char packet[7];
int profile = 2; // AAC LC AAC级别
int freqIdx = 11; // 采样频率: 8-16k ,11 8k, 4 - 44.1k, 3 - 48k
int chanCfg = 2; // CPE 频道数
// fill in ADTS data
packet[0] = 0xFF;
packet[1] = 0xF1;
packet[2] = (((profile - 1) << 6) + (freqIdx << 2) + (chanCfg >> 2));
packet[3] = (((chanCfg & 3) << 6) + (packetLen >> 11));
packet[4] = ((packetLen & 0x7FF) >> 3);
packet[5] = (((packetLen & 7) << 5) + 0x1F);
packet[6] = 0xFC;
return packet;
}
在每一帧aac数据保存前,先保存adts数据
//write to file
printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);
if(enc_ctx->codec_id == AV_CODEC_ID_AAC) {
fwrite(adts_gen(pkt->size), 7, 1, outfile);
}
fwrite(pkt->data, 1, pkt->size, outfile);
经过以上处理,重新保存aac文件,使用mediaInfo查看文件变大。
编码帧数为376,大小为 (376*7)/1024 = 2.57KB,接近增加的379-376=3KBd大小。此时的acc裸流文件,可以加入到QQ音乐并正常播放。
在前面的示例基础上,修改保存为AAC和MP3封装文件。
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#ifdef __cplusplus
}
#endif
static void encode(AVCodecContext *enc_ctx, AVFormatContext* fmt_ctx, const AVFrame *frame, AVPacket *pkt)
{
int ret;
/* send the frame to the encoder */
//if(frame)
// printf("Send frame %lld\n", frame->pts);
ret = avcodec_send_frame(enc_ctx, frame);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error sending a frame for encoding\n");
exit(1);
}
while(ret >= 0) {
ret = avcodec_receive_packet(enc_ctx, pkt);
if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return;
}
else if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error during encoding\n");
exit(1);
}
//write to file
printf("Write packet %lld (size = %d)\n", pkt->pts, pkt->size);
ret = av_write_frame(fmt_ctx, pkt);
if(ret < 0)
break;
av_packet_unref(pkt);
}
}
int main()
{
// 输入音频文件信息
int in_sample_rates = 8000;
uint64_t in_sample_layout = AV_CH_LAYOUT_STEREO;
AVSampleFormat in_sample_format = AV_SAMPLE_FMT_S16;
//const char *in_file_name = "../files/Titanic_48000_s16_stero.pcm";
const char *in_file_name = "../files/Titanic_8000_s16_stero.pcm";
// 输出编码流文件信息(和输入相同,否则pcm数据需要转换)
int out_sample_rates = 8000;
uint64_t out_ch_layout = AV_CH_LAYOUT_STEREO;
//AVCodecID codec_id = AV_CODEC_ID_AAC;
//AVSampleFormat out_sample_format = AV_SAMPLE_FMT_FLTP; // aac 固定使用FLTP
//const char *out_file_name = "Titanic_out.aac";
AVCodecID codec_id = AV_CODEC_ID_MP3;
AVSampleFormat out_sample_format = AV_SAMPLE_FMT_S16P; // mp3 固定使用S16p
const char *out_file_name = "Titanic_out.mp3";
输入输出文件
FILE *in_file = fopen(in_file_name, "rb");
if(!in_file) {
fprintf(stderr, "Can not open file %s\n", in_file_name);
return 0;
}
//AVFormatContext *fmt_ctx = avformat_alloc_context();
//fmt_ctx->oformat = av_guess_format(NULL, out_file_name, NULL);
AVFormatContext *fmt_ctx;
int ret = avformat_alloc_output_context2(&fmt_ctx, NULL, NULL, out_file_name);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "avformat_alloc_output_context2 failed.\n");
return 0;
}
if(avio_open(&fmt_ctx->pb, out_file_name, AVIO_FLAG_READ_WRITE) < 0) {
av_log(NULL, AV_LOG_ERROR, "avio_open failed.\n");
return 0;
}
// 输出流
AVStream *out_stream = avformat_new_stream(fmt_ctx, NULL);
video encoder
codec_id = fmt_ctx->oformat->audio_codec;
const AVCodec *encodec = avcodec_find_encoder(codec_id);
if(!encodec) {
av_log(NULL, AV_LOG_ERROR, "Codec '%s' not found\n", avcodec_get_name(codec_id));
return 0;
}
// video encoder contex
//AVCodecContext *encoder_ctx = out_stream->codec; //废弃的方法
//encoder_ctx->sample_rate = out_sample_rates;
//encoder_ctx->sample_fmt = out_sample_format;
//encoder_ctx->channel_layout = out_ch_layout;
// video encoder contex
AVCodecContext *encoder_ctx = avcodec_alloc_context3(encodec);
if(!encoder_ctx) {
av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context\n");
return 0;
}
// encoder parameters
encoder_ctx->sample_rate = out_sample_rates;
encoder_ctx->sample_fmt = out_sample_format;
encoder_ctx->channel_layout = out_ch_layout;
if(codec_id == AV_CODEC_ID_AAC)
encoder_ctx->bit_rate = 64000; // 1024/1152 * sizeof(float) * chns * 8bit= 65536/71728
// open condec
ret = avcodec_open2(encoder_ctx, encodec, NULL);
if(ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open codec\n");
avcodec_free_context(&encoder_ctx);
return 0;
}
新方法,配置输出codec上下文参数(在打开编码器后)
avcodec_parameters_from_context(out_stream->codecpar, encoder_ctx);
av_dump_format(fmt_ctx, 0, out_file_name, 1);
//Write Header
avformat_write_header(fmt_ctx, NULL);
// allocate variables
AVFrame *frame = av_frame_alloc();
//av_samples_alloc(frame->data, frame->linesize,
// encoder_ctx->channels, encoder_ctx->frame_size, encoder_ctx->sample_fmt, 1);
frame->channel_layout = encoder_ctx->channel_layout;
frame->nb_samples = encoder_ctx->frame_size;
frame->format = encoder_ctx->sample_fmt;
av_frame_get_buffer(frame, 1);
AVPacket *pkt = av_packet_alloc();
// start encoder
int64_t frame_cnt = 1;
while(!feof(in_file)) {
for(int i = 0; i < frame->nb_samples; i++) {
int16_t intens[2];
fread(intens, sizeof(int16_t) * 2, 1, in_file);
if(codec_id == AV_CODEC_ID_AAC) {
// s16 => fltp aac
*((float*)(frame->data[0]) + i) = intens[0] / 32768.;
*((float*)(frame->data[1]) + i) = intens[1] / 32768.;
}
else if(codec_id == AV_CODEC_ID_MP3) {
// s16 => s16p mp3
*((int16_t*)(frame->data[0]) + i) = intens[0];
*((int16_t*)(frame->data[1]) + i) = intens[1];
}
}
frame->pts = frame_cnt++;
encode(encoder_ctx, fmt_ctx, frame, pkt);
}
// flush
encode(encoder_ctx, fmt_ctx, NULL, pkt);
//Write Trailer
av_write_trailer(fmt_ctx);
fclose(in_file);
avcodec_free_context(&encoder_ctx);
av_frame_free(&frame);
av_packet_free(&pkt);
avio_close(fmt_ctx->pb);
avformat_free_context(fmt_ctx);
}
先查看aac文件,不同意上面保存的裸流文件无法查看文件详细信息,这里以封装格式保存能看到详细的文件信息,例如文件格式ADTS,声道、码率、采样率等。