本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584948.html

FFmpeg 编解码处理系列笔记:

[0]. FFmpeg时间戳详解

[1]. FFmpeg编解码处理1-转码全流程简介

[2]. FFmpeg编解码处理2-编解码API详解

[3]. FFmpeg编解码处理3-视频编码

[4]. FFmpeg编解码处理4-音频编码

基于 FFmpeg 4.1 版本。

6. 音频编码

编码使用 avcodec_send_frame() 和 avcodec_receive_packet() 两个函数。

音频编码的步骤:

[1] 初始化打开输出文件时构建编码器上下文

[2] 音频帧编码

[2.1] 将滤镜输出的音频帧写入音频 FIFO

[2.2] 按音频编码器中要求的音频帧尺寸从音频 FIFO 中取出音频帧

[2.3] 为音频帧生成 pts

[2.4] 将音频帧送入编码器,从编码器取出编码帧

[2.5] 更新编码帧流索引

[2.6] 将帧中时间参数按输出封装格式的时间基进行转换

6.1 打开视频编码器

完整源码在 open_output_file() 函数中,下面摘出关键部分:

    // 3. 构建AVCodecContext
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO ||
dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) // 音频流或视频流
{
// 3.1 查找编码器AVCodec,本例使用与解码器相同的编码器
AVCodec *encoder = NULL;
if ((dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO) && (strcmp(v_enc_name, "copy") != 0))
{
encoder = avcodec_find_encoder_by_name(v_enc_name);
}
else if ((dec_ctx->codec_type == AVMEDIA_TYPE_AUDIO) && (strcmp(a_enc_name, "copy") != 0))
{
encoder = avcodec_find_encoder_by_name(a_enc_name);
}
else
{
encoder = avcodec_find_encoder(dec_ctx->codec_id);
} if (!encoder)
{
av_log(NULL, AV_LOG_FATAL, "Necessary encoder not found\n");
return AVERROR_INVALIDDATA;
}
// 3.2 AVCodecContext初始化:分配结构体,使用AVCodec初始化AVCodecContext相应成员为默认值
AVCodecContext *enc_ctx = avcodec_alloc_context3(encoder);
if (!enc_ctx)
{
av_log(NULL, AV_LOG_FATAL, "Failed to allocate the encoder context\n");
return AVERROR(ENOMEM);
} // 3.3 AVCodecContext初始化:配置图像/声音相关属性
/* In this example, we transcode to same properties (picture size,
* sample rate etc.). These properties can be changed for output
* streams easily using filters */
if (dec_ctx->codec_type == AVMEDIA_TYPE_VIDEO)
{
enc_ctx->height = dec_ctx->height; // 图像高
enc_ctx->width = dec_ctx->width; // 图像宽
enc_ctx->sample_aspect_ratio = dec_ctx->sample_aspect_ratio; // 采样宽高比:像素宽/像素高
/* take first format from list of supported formats */
if (encoder->pix_fmts) // 编码器支持的像素格式列表
{
enc_ctx->pix_fmt = encoder->pix_fmts[0]; // 编码器采用所支持的第一种像素格式
}
else
{
enc_ctx->pix_fmt = dec_ctx->pix_fmt; // 编码器采用解码器的像素格式
}
/* video time_base can be set to whatever is handy and supported by encoder */
enc_ctx->time_base = av_inv_q(dec_ctx->framerate); // 时基:解码器帧率取倒数
enc_ctx->framerate = dec_ctx->framerate;
//enc_ctx->bit_rate = dec_ctx->bit_rate; /* emit one intra frame every ten frames
* check frame pict_type before passing frame
* to encoder, if frame->pict_type is AV_PICTURE_TYPE_I
* then gop_size is ignored and the output of encoder
* will always be I frame irrespective to gop_size
*/
//enc_ctx->gop_size = 10;
//enc_ctx->max_b_frames = 1;
}
else
{
enc_ctx->sample_rate = dec_ctx->sample_rate; // 采样率
enc_ctx->channel_layout = dec_ctx->channel_layout; // 声道布局
enc_ctx->channels = av_get_channel_layout_nb_channels(enc_ctx->channel_layout); // 声道数量
/* take first format from list of supported formats */
enc_ctx->sample_fmt = encoder->sample_fmts[0]; // 编码器采用所支持的第一种采样格式
enc_ctx->time_base = (AVRational){1, enc_ctx->sample_rate}; // 时基:编码器采样率取倒数
// enc_ctx->codec->capabilities |= AV_CODEC_CAP_VARIABLE_FRAME_SIZE; // 只读标志 // 初始化一个FIFO用于存储待编码的音频帧,初始化FIFO大小的1个采样点
// av_audio_fifo_alloc()第二个参数是声道数,第三个参数是单个声道的采样点数
// 采样格式及声道数在初始化FIFO时已设置,各处涉及FIFO大小的地方都是用的单个声道的采样点数
pp_audio_fifo[i] = av_audio_fifo_alloc(enc_ctx->sample_fmt, enc_ctx->channels, 1);
if (pp_audio_fifo == NULL)
{
av_log(NULL, AV_LOG_ERROR, "Could not allocate FIFO\n");
return AVERROR(ENOMEM);
}
} if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
{
enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
} // 3.4 AVCodecContext初始化:使用AVCodec初始化AVCodecContext,初始化完成
/* Third parameter can be used to pass settings to encoder */
ret = avcodec_open2(enc_ctx, encoder, NULL);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Cannot open video encoder for stream #%u\n", i);
return ret;
}
// 3.5 设置输出流codecpar
ret = avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
if (ret < 0)
{
av_log(NULL, AV_LOG_ERROR, "Failed to copy encoder parameters to output stream #%u\n", i);
return ret;
} // 3.6 保存输出流contex
pp_enc_ctx[i] = enc_ctx;
}

6.2 判断是否需要音频 FIFO

完整源码在 main() 函数中,下面摘出关键部分:

    if (codec_type == AVMEDIA_TYPE_AUDIO) {
if (((stream.o_codec_ctx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) == 0) &&
(stream.i_codec_ctx->frame_size != stream.o_codec_ctx->frame_size))
{
stream.aud_fifo = oafifo[stream_index];
ret = transcode_audio_with_afifo(&stream, &ipacket);
}
else
{
ret = transcode_audio(&stream, &ipacket);
}
}

解码过程中的音频帧尺寸:

AVCodecContext.frame_size 表示音频帧中每个声道包含的采样点数。当编码器 AV_CODEC_CAP_VARIABLE_FRAME_SIZE 标志有效时,音频帧尺寸是可变的,AVCodecContext.frame_size 值可能为 0;否则,解码器的 AVCodecContext.frame_size 等于解码帧中的 AVFrame.nb_samples。

编码过程中的音频帧尺寸:

上述代码中第一个判断条件是 "(stream.o_codec_ctx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE) == 0)", 第二个判断条件是 "(stream.i_codec_ctx->frame_size != stream.o_codec_ctx->frame_size)"。如果编码器不支持可变尺寸音频帧(第一个判断条件生效),而原始音频帧的尺寸又和编码器帧尺寸不一样(第二个判断条件生效),则需要引入音频帧 FIFO,以保证每次从 FIFO 中取出的音频帧尺寸和编码器帧尺寸一样。音频 FIFO 输出的音频帧不含时间戳信息,因此需要重新生成时间戳。

引入音频FIFO的原因:

如果编码器不支持可变长度帧,而编码器输入音频帧尺寸和编码器要求的音频帧尺寸不一样,就会编码失败。比如,AAC 音频格式转 MP2 音频格式,AAC 格式音频帧尺寸为 1024,而 MP2 音频编码器要求音频帧尺寸为 1152,编码会失败;再比如 AAC 格式转码 AAC 格式,某些 AAC 音频帧为 2048,而此时若 AAC 音频编码器要求音频帧尺寸为 1024,编码就会失败。解决这个问题的方法有两个,一是进行音频重采样,使音频帧转换为编码器支持的格式;另一个是引入音频 FIFO,一端写一端读,每次从读端取出编码器要求的帧尺寸即可。

AAC 音频帧尺寸可能是 1024,也可能是 2048,参考“FFmpeg关于nb_smples,frame_size以及profile的解释

6.3 音频 FIFO 接口函数

本节代码参考 "https://github.com/FFmpeg/FFmpeg/blob/n4.1/doc/examples/transcode_aac.c" 实现

/**
* Initialize one input frame for writing to the output file.
* The frame will be exactly frame_size samples large.
* @param[out] frame Frame to be initialized
* @param output_codec_context Codec context of the output file
* @param frame_size Size of the frame
* @return Error code (0 if successful)
*/
static int init_audio_output_frame(AVFrame **frame,
AVCodecContext *occtx,
int frame_size)
{
int error; /* Create a new frame to store the audio samples. */
if (!(*frame = av_frame_alloc()))
{
fprintf(stderr, "Could not allocate output frame\n");
return AVERROR_EXIT;
} /* Set the frame's parameters, especially its size and format.
* av_frame_get_buffer needs this to allocate memory for the
* audio samples of the frame.
* Default channel layouts based on the number of channels
* are assumed for simplicity. */
(*frame)->nb_samples = frame_size;
(*frame)->channel_layout = occtx->channel_layout;
(*frame)->format = occtx->sample_fmt;
(*frame)->sample_rate = occtx->sample_rate; /* Allocate the samples of the created frame. This call will make
* sure that the audio frame can hold as many samples as specified. */
// 为AVFrame分配缓冲区,此函数会填充AVFrame.data和AVFrame.buf,若有需要,也会填充
// AVFrame.extended_data和AVFrame.extended_buf,对于planar格式音频,会为每个plane
// 分配一个缓冲区
if ((error = av_frame_get_buffer(*frame, 0)) < 0)
{
fprintf(stderr, "Could not allocate output frame samples (error '%s')\n",
av_err2str(error));
av_frame_free(frame);
return error;
} return 0;
} // FIFO中可读数据小于编码器帧尺寸,则继续往FIFO中写数据
static int write_frame_to_audio_fifo(AVAudioFifo *fifo,
uint8_t **new_data,
int new_size)
{
int ret = av_audio_fifo_realloc(fifo, av_audio_fifo_size(fifo) + new_size);
if (ret < 0)
{
fprintf(stderr, "Could not reallocate FIFO\n");
return ret;
} /* Store the new samples in the FIFO buffer. */
ret = av_audio_fifo_write(fifo, (void **)new_data, new_size);
if (ret < new_size)
{
fprintf(stderr, "Could not write data to FIFO\n");
return AVERROR_EXIT;
} return 0;
} static int read_frame_from_audio_fifo(AVAudioFifo *fifo,
AVCodecContext *occtx,
AVFrame **frame)
{
AVFrame *output_frame;
// 如果FIFO中可读数据多于编码器帧大小,则只读取编码器帧大小的数据出来
// 否则将FIFO中数据读完。frame_size是帧中单个声道的采样点数
const int frame_size = FFMIN(av_audio_fifo_size(fifo), occtx->frame_size); /* Initialize temporary storage for one output frame. */
// 分配AVFrame及AVFrame数据缓冲区
int ret = init_audio_output_frame(&output_frame, occtx, frame_size);
if (ret < 0)
{
return AVERROR_EXIT;
} // 从FIFO从读取数据填充到output_frame->data中
ret = av_audio_fifo_read(fifo, (void **)output_frame->data, frame_size);
if (ret < frame_size)
{
fprintf(stderr, "Could not read data from FIFO\n");
av_frame_free(&output_frame);
return AVERROR_EXIT;
} *frame = output_frame; return ret;
}

6.4 编码音频帧

完整源码在 transcode_audio_with_afifo() 函数中,下面摘出关键部分:

    // 2. 滤镜处理
ret = filtering_frame(sctx->flt_ctx, frame_dec, frame_flt);
if (ret == AVERROR_EOF) // 滤镜已冲洗
{
flt_finished = true;
av_log(NULL, AV_LOG_INFO, "filtering aframe EOF\n");
frame_flt = NULL;
}
else if (ret < 0)
{
av_log(NULL, AV_LOG_INFO, "filtering aframe error %d\n", ret);
goto end;
} // 3. 使用音频fifo,从而保证每次送入编码器的音频帧尺寸满足编码器要求
// 3.1 将音频帧写入fifo,音频帧尺寸是解码格式中音频帧尺寸
if (!dec_finished)
{
uint8_t** new_data = frame_flt->extended_data; // 本帧中多个声道音频数据
int new_size = frame_flt->nb_samples; // 本帧中单个声道的采样点数 // FIFO中可读数据小于编码器帧尺寸,则继续往FIFO中写数据
ret = write_frame_to_audio_fifo(p_fifo, new_data, new_size);
if (ret < 0)
{
av_log(NULL, AV_LOG_INFO, "write aframe to fifo error\n");
goto end;
}
} // 3.2 从fifo中取出音频帧,音频帧尺寸是编码格式中音频帧尺寸
// FIFO中可读数据大于编码器帧尺寸,则从FIFO中读走数据进行处理
while ((av_audio_fifo_size(p_fifo) >= enc_frame_size) || dec_finished)
{
bool flushing = dec_finished && (av_audio_fifo_size(p_fifo) == 0); // 已取空,刷洗编码器 if (frame_enc != NULL)
{
av_frame_free(&frame_enc);
} if (!flushing)
{
// 从FIFO中读取数据,编码,写入输出文件
ret = read_frame_from_audio_fifo(p_fifo, sctx->o_codec_ctx, &frame_enc);
if (ret < 0)
{
av_log(NULL, AV_LOG_INFO, "read aframe from fifo error\n");
goto end;
} // 4. fifo中读取的音频帧没有时间戳信息,重新生成pts
frame_enc->pts = s_pts;
s_pts += ret;
} flush_encoder:
// 5. 编码
ret = av_encode_frame(sctx->o_codec_ctx, frame_enc, &opacket);
if (ret == AVERROR(EAGAIN)) // 需要获取新的frame喂给编码器
{
//av_log(NULL, AV_LOG_INFO, "encode aframe need more packet\n");
if (frame_enc != NULL)
{
av_frame_free(&frame_enc);
}
continue;
}
else if (ret == AVERROR_EOF)
{
av_log(NULL, AV_LOG_INFO, "encode aframe EOF\n");
enc_finished = true;
goto end;
} // 5.1 更新编码帧中流序号,并进行时间基转换
// AVPacket.pts和AVPacket.dts的单位是AVStream.time_base,不同的封装格式其AVStream.time_base不同
// 所以输出文件中,每个packet需要根据输出封装格式重新计算pts和dts
opacket.stream_index = sctx->stream_idx;
av_packet_rescale_ts(&opacket, sctx->o_codec_ctx->time_base, sctx->o_stream->time_base); av_log(NULL, AV_LOG_DEBUG, "Muxing frame\n"); // 6. 将编码后的packet写入输出媒体文件
ret = av_interleaved_write_frame(sctx->o_fmt_ctx, &opacket);
if (ret < 0)
{
av_log(NULL, AV_LOG_INFO, "write aframe error %d\n", ret);
goto end;
} if (flushing)
{
goto flush_encoder;
}
}

FFmpeg编解码处理4-音频编码的更多相关文章

  1. FFmpeg编解码处理3-视频编码

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584937.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...

  2. FFmpeg编解码处理2-编解码API详解

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584925.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...

  3. FFmpeg编解码处理1-转码全流程简介

    本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10584901.html FFmpeg编解码处理系列笔记: [0]. FFmpeg时间戳详 ...

  4. ffmpeg 编解码详细过程

    ffmpeg编解码详细过程     bobbypollo 转:ffmpeg编解码详细过程 原文地址:ffmpeg编解码详细过程(转)作者:心在飞翔原文出处: http://www.360doc.com ...

  5. 最近在研究FFmpeg编解码

    好几年没上CNBLOGS了, 最近在研究FFmpeg编解码,一个人研究感到很寂寞,所以想通过博客来和大家分享和交流,呵呵. 最近研究的主题是: ANDROID手机同屏技术: 需要用到ANDROID截屏 ...

  6. ffmpeg编解码音频AAC

    本次项目的需求:手机端和PC端共享同一个音视频网络源. 所以编解码需要满足手机上编码和解码原来PC端的音视频流. 这里先封装安卓手机端音频的编解码. 编译工作依然是在linux下 ubuntu 12. ...

  7. ffmpeg编解码视频导致噪声增大的一种解决方法

    一.前言 ffmpeg在视音频编解码领域算是一个比较成熟的解决方案了.公司的一款视频编辑软件正是基于ffmpeg做了二次封装,并在此基础上进行音视频的编解码处理.然而,在观察编码后的视频质量时,发现图 ...

  8. 流媒体技术学习笔记之(六)FFmpeg官方文档先进音频编码(AAC)

    先进音频编码(AAC)的后继格式到MP3,和以MPEG-4部分3(ISO / IEC 14496-3)被定义.它通常用于MP4容器格式; 对于音乐,通常使用.m4a扩展名.第二最常见的用途是在MKV( ...

  9. FFmpeg 学习(五):FFmpeg 编解码 API 分析

    在上一篇文章 FFmpeg学习(四):FFmpeg API 介绍与通用 API 分析 中,我们简单的讲解了一下FFmpeg 的API基本概念,并分析了一下通用API,本文我们将分析 FFmpeg 在编 ...

随机推荐

  1. 别人的Linux私房菜(12)正则表达式与文件格式化处理

    vi gerp awk sed支持正则表达式   cp ls不支持,只能使用bash本身的通配符 正则表达式分为基础正则表达式和拓展正则表达式 使用正则表达式注意语系的影响 http://cn.lin ...

  2. kbmmw中向服务器端传递对象的一种简单方式

    运行环境:delphi 10.2+kbmmw 5.6.20 在kbmmw 的老版本中,要向服务器传送一个本地的对象,一般都需要进行一些转换,例如通过序列化的方式. 在新版的kbmmw中这一切都变的很简 ...

  3. Ubuntu 14.04 LTS 下使用校园网客户端DrclientLinux

    原先博客放弃使用,几篇文章搬运过来 下载客户端并解压 安装开发包 sudo -i dpkg --add-architecture i386 #添加32位的支持 apt-get update apt-g ...

  4. 【python-strip】Python strip()方法

    strip()方法用于移除字符串首尾的指定字符(默认是空格) 比如: str = "0000000this is string example....wow!!!0000000"; ...

  5. Centos7中docker开启远程访问

    在作为docker远程服务的centos7机器中配置: 1.在/usr/lib/systemd/system/docker.service,配置远程访问.主要是在[Service]这个部分,加上下面两 ...

  6. [UWP]使用Popup构建UWP Picker

    在上一篇博文<[UWP]不那么好用的ContentDialog>中我们讲到了ContentDialog在复杂场景下使用的几个令人头疼的弊端.那么,就让我们在这篇博文里开始愉快的造轮子之旅吧 ...

  7. Laravel在进行表单验证时,错误信息未返回

    马上要毕业了,找了现在的这家公司,压力不大,自己也比较喜欢,唯一的遗憾就是手机号莫得换了(找不到换的借口). 进入正题: 之前自己的博客(http://lxiaoke.cn)是用ThinkPHP开发的 ...

  8. 写你的shell,其实很简单[架构篇]

    引语:我本人以前并没有写过shell脚本,也许是因为懒,也许是没有被逼到要去写shell的地步.但是,前段时间,工作需求,要求重新跑几个月的脚本,这些脚本是每天定时进行跑的,而且每天是好几个脚本一起关 ...

  9. Post传值到后台经典场景(C#)

    经典场景 传输内容包含 文件 注意事项:类型必须为form-data [HttpPost] [Route("api/Test")] public JsonResult Test(s ...

  10. Spring Boot Starters启动器

    Starters是什么? Starters可以理解为启动器,它包含了一系列可以集成到应用里面的依赖包,你可以一站式集成Spring及其他技术,而不需要到处找示例代码和依赖包.如你想使用Spring J ...