音频解码过程
⾳频解码过程如下图所示:
FFmpeg流程
关键函数
关键函数说明:
- avcodec_find_decoder:根据指定的AVCodecID查找注册的解码器。
- av_parser_init:初始化AVCodecParserContext。
- avcodec_alloc_context3:为AVCodecContext分配内存。
- avcodec_open2:打开解码器。
- av_parser_parse2:解析获得⼀个Packet。
- avcodec_send_packet:将AVPacket压缩数据给解码器。
- avcodec_receive_frame:获取到解码后的AVFrame数据。
- av_get_bytes_per_sample: 获取每个sample中的字节数。
关键数据结构
AVCodecParser:⽤于解析输⼊的数据流并把它分成⼀帧⼀帧的压缩编码数据。⽐较形象的说法就是把⻓⻓的⼀段连续的数据“切割”成⼀段段的数据。⽐如AAC aac_parser
ffmpeg-4.2.1\libavcodec\aac_parser.c
AVCodecParser ff_aac_parser = {
codec_ids = { AV_CODEC_ID_AAC },
priv_data_size = sizeof(AACAC3ParseContext),
parser_init = aac_parse_init,
parser_parse = ff_aac_ac3_parse,
parser_close = ff_parse_close,
};
从AVCodecParser结构的实例化我们可以看出来,不同编码类型的parser是和CODE_ID进⾏绑定的。所以也就可以解释
parser = av_parser_init(codec->id);
可以通过CODE_ID查找到对应的码流 parser。
avcodec编解码API介绍
- avcodec_send_packet、avcodec_receive_frame的API是FFmpeg3版本加⼊的。
- 为了正确的使⽤它们,有必要阅读FFmpeg的⽂档说明:FFmpeg: send/receive encoding and decoding API overview
以下内容摘译⾃⽂档说明
FFmpeg提供了两组函数,分别⽤于编码和解码:
- 解码:avcodec_send_packet()、avcodec_receive_frame()。
- 解码:avcodec_send_frame()、avcodec_receive_packet()。
API的设计与编解码的流程⾮常贴切。
建议的使⽤流程如下:
- 像以前⼀样设置并打开AVCodecContext。
- 输⼊有效的数据:
- 解码:调⽤avcodec_send_packet()给解码器传⼊包含原始的压缩数据的AVPacket对象。
- 编码:调⽤ avcodec_send_frame()给编码器传⼊包含解压数据的AVFrame对象。
两种情况下推荐AVPacket和AVFrame都使⽤refcounted(引⽤计数)的模式,否则libavcodec可能不得不对输⼊的数据进⾏拷⻉。
- 在⼀个循环体内去接收codec的输出,即周期性地调⽤avcodec_receive_*()来接收codec输出的数据:
- 解码:调⽤avcodec_receive_frame(),如果成功会返回⼀个包含未压缩数据的AVFrame。
- 编码:调⽤avcodec_receive_packet(),如果成功会返回⼀个包含压缩数据的AVPacket。
反复地调⽤avcodec_receive_packet()直到返回 AVERROR(EAGAIN)或其他错误。返回AVERROR(EAGAIN)错误表示codec需要新的输⼊来输出更多的数据。对于每个输⼊的packet或frame,codec⼀般会输出⼀个frame或packet,但是也有可能输出0个或者多于1个。
- 流处理结束的时候需要flush(冲刷) codec。因为codec可能在内部缓冲多个frame或packet,出于性能或其他必要的情况(如考虑B帧的情况)。
处理流程如下:
- 调⽤avcodec_send_*()传⼊的AVFrame或AVPacket指针设置为NULL。 这将进⼊draining mode(排⽔模式)。
- 反复地调⽤avcodec_receive_*()直到返回AVERROR_EOF,该⽅法在draining mode时不会返回AVERROR(EAGAIN)的错误,除⾮你没有进⼊draining mode。
- 当重新开启codec时,需要先调⽤ avcodec_flush_buffers()来重置codec。
说明:
- 编码或者解码刚开始的时候,codec可能接收了多个输⼊的frame或packet后还没有输出数据,直到内部的buffer被填充满。上⾯的使⽤流程可以处理这种情况。
- 理论上,只有在输出数据没有被完全接收的情况调⽤avcodec_send_*()的时候才可能会发⽣AVERROR(EAGAIN)的错误。你可以依赖这个机制来实现区别于上⾯建议流程的处理⽅式,⽐如每次循环都调⽤avcodec_send_*(),在出现AVERROR(EAGAIN)错误的时候再去调⽤avcodec_receive_*()。
- 并不是所有的codec都遵循⼀个严格、可预测的数据处理流程,唯⼀可以保证的是 “调⽤avcodec_send_*()/avcodec_receive_*()返回AVERROR(EAGAIN)的时候去avcodec_receive_*()/avcodec_send_*()会成功,否则不应该返回AVERROR(EAGAIN)的错误。”⼀般来说,任何codec都不允许⽆限制地缓存输⼊或者输出。
- 在同⼀个AVCodecContext上混合使⽤新旧API是不允许的,这将导致未定义的⾏为。
avcodec_send_packet
函数:
int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
作用:
- ⽀持将裸流数据包送给解码器
警告:
- 输⼊的avpkt-data缓冲区必须⼤于AV_INPUT_PADDING_SIZE,因为优化的字节流读取器必须⼀次读取32或者64⽐特的数据
- 不能跟之前的API(例如avcodec_decode_video2)混⽤,否则会返回不可预知的错误
备注:
- 在将包发送给解码器的时候,AVCodecContext必须已经通过avcodec_open2打开
参数:
- avctx:解码上下⽂
- avpkt:输⼊AVPakcet.通常情况下,输⼊数据是⼀个单⼀的视频帧或者⼏个完整的⾳频帧。调⽤者保留包的原有属性,解码器不会修改包的内容。解码器可能创建对包的引⽤。如果包没有引⽤计数将拷⻉⼀份。跟以往的API不⼀样,输⼊的包的数据将被完全地消耗,如果包含有多个帧,要求多次调⽤avcodec_recvive_frame,直到avcodec_recvive_frame返回VERROR(EAGAIN)或AVERROR_EOF。输⼊参数可以为NULL,或者AVPacket的data域设置为NULL或者size域设置为0,表示将刷新所有的包,意味着数据流已经结束了。第⼀次发送刷新会总会成功,第⼆次发送刷新包是没有必要的,并且返回AVERROR_EOF,如果×××缓存了⼀些帧,返回⼀个刷新包,将会返回所有的解码包
返回值:
- 0: 表示成功
- AVERROR(EAGAIN):当前状态不接受输⼊,⽤户必须先使⽤avcodec_receive_frame() 读取数据帧;
- AVERROR_EOF:解码器已刷新,不能再向其发送新包;
- AVERROR(EINVAL):没有打开解码器,或者这是⼀个编码器,或者要求刷新;
- AVERRO(ENOMEN):⽆法将数据包添加到内部队列。
avcodec_receive_frame
函数:
int avcodec_receive_frame ( AVCodecContext * avctx, AVFrame * frame )
作⽤:
- 从解码器返回已解码的输出数据。
参数:
- avctx: 编解码器上下⽂
- frame: 获取使⽤reference-counted机制的audio或者video帧(取决于解码器类型)。请注意,在执⾏其他操作之前,函数内部将始终先调⽤av_frame_unref(frame)。
返回值:
- 0: 成功,返回⼀个帧
- AVERROR(EAGAIN): 该状态下没有帧输出,需要使⽤avcodec_send_packet发送新的packet到解码器
- AVERROR_EOF: 解码器已经被完全刷新,不再有输出帧
- AVERROR(EINVAL): 编解码器没打开
- 其他<0的值: 具体查看对应的错误码
实现流程
准备文件
将aac
和mp3
文件放在build
目录下
添加main
函数参数,表示输入文件和输出文件
读取文件
使用二进制读取文件
// 打开输入文件
infile = fopen(filename, "rb");
if (!infile) {
fprintf(stderr, "Could not open %s\n", filename);
exit(1);
}
查找解码器
- 根据不同的格式使用不同的解码器
ID
enum AVCodecID audio_codec_id = AV_CODEC_ID_AAC;
if(strstr(filename, "aac") != NULL)
{
audio_codec_id = AV_CODEC_ID_AAC;
}
else if(strstr(filename, "mp3") != NULL)
{
audio_codec_id = AV_CODEC_ID_MP3;
}
- 根据解码器
ID
查找解码器,填充解码器信息
const AVCodec *codec;
codec = avcodec_find_decoder(audio_codec_id);
- 分配解码器上下文,将解码器信息拷贝到解码器上下文
codec_ctx = avcodec_alloc_context3(codec);
- 打开解码器上下文,将解码器和解码器上下文关联
avcodec_open2(codec_ctx, codec, NULL)
初始化裸流解析器
- 需要解析为
pcm
,因此需要使用到裸流解析器 - 根据不同的解码器
ID
分配不同裸流解析器,填充解析器信息
AVCodecParserContext *parser = NULL;
parser = av_parser_init(codec->id);
打开输出文件
// 打开输出文件
FILE *outfile = NULL;
outfile = fopen(outfilename, "wb");
if (!outfile) {
av_free(codec_ctx);
exit(1);
}
- 开始读取文件内容、
AUDIO_INBUF_SIZE
定义了单次最多读取的数量AV_INPUT_BUFFER_PADDING_SIZE
宏为64
,主要是防止一些编解码器适配问题而添加了尾部长度
#define AUDIO_INBUF_SIZE 20480
uint8_t inbuf[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
uint8_t *data = NULL;
data = inbuf;
data_size = fread(inbuf, 1, AUDIO_INBUF_SIZE, infile);
循环读取文件内容
- 分配帧内存,如果分配失败,则进行相关错误操作
AVFrame *decoded_frame = NULL;
if (!decoded_frame)
{
if (!(decoded_frame = av_frame_alloc()))
{
fprintf(stderr, "Could not allocate audio frame\n");
exit(1);
}
}
- 解码出的数据是一段连续的数据裸流,因此需要使用裸流解析器进行分开
av_parser_parse2
函数用于将裸流数据解析成一帧一帧的数据包存入在packet
中,AV_NOPTS_VALUE
表示没有时间戳dts
和pts
,因为需要解析出的是pcm
流,不存在时间戳ret
返回的是已经解析了的数据大小,也就是说传进去的数据不一定全部被解析完,存在碎片信息
AVPacket *pkt = NULL;
pkt = av_packet_alloc();
ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,
data, data_size,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0)
- 根据返回的碎片大小,调整文件数据指针
data += ret; // 跳过已经解析的数据
data_size -= ret; // 对应的缓存大小也做相应减小
- 对
packet
包的数据进行解码
if (pkt->size)
decode(codec_ctx, pkt, decoded_frame, outfile);
decode
函数
- 主要是使用
avcodec_send_packet
发送数据包,以及avcodec_receive_frame
接收数据包 - 需要判断一下返回值,根据不同返回值进行错误处理或者进行下一步解码
avcodec_send_packet
ret = avcodec_send_packet(dec_ctx, pkt);
if(ret == AVERROR(EAGAIN))
{
fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
}
else if (ret < 0)
{
fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
av_get_err(ret), pkt->size);
// exit(1);
return;
}
avcodec_receive_frame
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0)
{
fprintf(stderr, "Error during decoding\n");
exit(1);
}
- 写入文件
- 根据不同的音频格式获取一个样本所占的字节数
int data_size;
data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
- 根据不同的声道数,循环写入数据
- 这样写入的类型是
float
类型
for (i = 0; i < frame->nb_samples; i++)
{
for (ch = 0; ch < dec_ctx->channels; ch++) // 交错的方式写入, 大部分float的格式输出
fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);
}
- 循环读入文件,直到缓冲区
buf
的内存少于我们设置的阈值后,重新读取文件信息 - 首先将没读取完的数据放到缓冲区头部,然后再读取剩下的大小
memove
函数的作用是将data
开始的data_size
长度字节的数据拷贝到inbuf
头部
#define AUDIO_REFILL_THRESH 4096
if (data_size < AUDIO_REFILL_THRESH) // 如果数据少了则再次读取
{
memmove(inbuf, data, data_size); // 把之前剩的数据拷贝到buffer的起始位置
data = inbuf;
// 读取数据 长度: AUDIO_INBUF_SIZE - data_size
len = fread(data + data_size, 1, AUDIO_INBUF_SIZE - data_size, infile);
if (len > 0)
data_size += len;
}
冲刷解码器
- 如果读取结束了,就退出循环
while (data_size > 0)
- 退出读取后需要冲刷解码器,让它进入
drain mode
drain mode
是什么? - 在使用 FFmpeg 进行音视频解码时,当你已经将所有的编码数据包(
AVPacket
)都发送给解码器后,解码器内部可能仍然存在一些已经接收但还未完全解码的数据。这可能是由于编解码器的特性,例如存在延迟解码的情况(像一些视频编码格式中,为了提高压缩效率,会使用 B 帧(双向预测帧),这就可能导致解码过程存在延迟)。“drain mode” 就是用于处理这种情况,确保解码器将内部所有剩余的数据都解码输出。
pkt->data = NULL; // 让其进入drain mode
pkt->size = 0;
decode(codec_ctx, pkt, decoded_frame, outfile);
释放内存
- 读取结束后需要释放相关上下文内存
avcodec_free_context(&codec_ctx);
av_parser_close(parser);
av_frame_free(&decoded_frame);
av_packet_free(&pkt);
- 关闭打开的文件
fclose(outfile);
fclose(infile);
完整代码
main.c
/**
* @projectName 07-05-decode_audio
* @brief 解码音频,主要的测试格式aac和mp3
* @author Liao Qingfu
* @date 2020-01-16
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libavutil/frame.h>
#include <libavutil/mem.h>
#include <libavcodec/avcodec.h>
#define AUDIO_INBUF_SIZE 20480
#define AUDIO_REFILL_THRESH 4096
static char err_buf[128] = {0};
static char* av_get_err(int errnum)
{
av_strerror(errnum, err_buf, 128);
return err_buf;
}
static void print_sample_format(const AVFrame *frame)
{
printf("ar-samplerate: %uHz\n", frame->sample_rate);
printf("ac-channel: %u\n", frame->channels);
printf("f-format: %u\n", frame->format);// 格式需要注意,实际存储到本地文件时已经改成交错模式
}
static void decode(AVCodecContext *dec_ctx, AVPacket *pkt, AVFrame *frame,
FILE *outfile)
{
int i, ch;
int ret, data_size;
/* send the packet with the compressed data to the decoder */
ret = avcodec_send_packet(dec_ctx, pkt);
if(ret == AVERROR(EAGAIN))
{
fprintf(stderr, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");
}
else if (ret < 0)
{
fprintf(stderr, "Error submitting the packet to the decoder, err:%s, pkt_size:%d\n",
av_get_err(ret), pkt->size);
// exit(1);
return;
}
/* read all the output frames (infile general there may be any number of them */
while (ret >= 0)
{
// 对于frame, avcodec_receive_frame内部每次都先调用
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0)
{
fprintf(stderr, "Error during decoding\n");
exit(1);
}
data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
if (data_size < 0)
{
/* This should not occur, checking just for paranoia */
fprintf(stderr, "Failed to calculate data size\n");
exit(1);
}
static int s_print_format = 0;
if(s_print_format == 0)
{
s_print_format = 1;
print_sample_format(frame);
}
/**
P表示Planar(平面),其数据格式排列方式为 :
LLLLLLRRRRRRLLLLLLRRRRRRLLLLLLRRRRRRL...(每个LLLLLLRRRRRR为一个音频帧)
而不带P的数据格式(即交错排列)排列方式为:
LRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRLRL...(每个LR为一个音频样本)
播放范例: ffplay -ar 48000 -ac 2 -f f32le believe.pcm
*/
for (i = 0; i < frame->nb_samples; i++)
{
for (ch = 0; ch < dec_ctx->channels; ch++) // 交错的方式写入, 大部分float的格式输出
fwrite(frame->data[ch] + data_size*i, 1, data_size, outfile);
}
}
}
// 播放范例: ffplay -ar 48000 -ac 2 -f f32le believe.pcm
int main(int argc, char **argv)
{
const char *outfilename;
const char *filename;
const AVCodec *codec;
AVCodecContext *codec_ctx= NULL;
AVCodecParserContext *parser = NULL;
int len = 0;
int ret = 0;
FILE *infile = NULL;
FILE *outfile = NULL;
uint8_t inbuf[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
uint8_t *data = NULL;
size_t data_size = 0;
AVPacket *pkt = NULL;
AVFrame *decoded_frame = NULL;
if (argc <= 2)
{
fprintf(stderr, "Usage: %s <input file> <output file>\n", argv[0]);
exit(0);
}
filename = argv[1];
outfilename = argv[2];
pkt = av_packet_alloc();
enum AVCodecID audio_codec_id = AV_CODEC_ID_AAC;
if(strstr(filename, "aac") != NULL)
{
audio_codec_id = AV_CODEC_ID_AAC;
}
else if(strstr(filename, "mp3") != NULL)
{
audio_codec_id = AV_CODEC_ID_MP3;
}
else
{
printf("error reading file!\n");
printf("default codec id:%d\n", audio_codec_id);
}
// 查找解码器
codec = avcodec_find_decoder(audio_codec_id); // AV_CODEC_ID_AAC
if (!codec) {
fprintf(stderr, "Codec not found\n");
exit(1);
}
// 获取裸流的解析器 AVCodecParserContext(数据) + AVCodecParser(方法)
parser = av_parser_init(codec->id);
if (!parser) {
fprintf(stderr, "Parser not found\n");
exit(1);
}
// 分配codec上下文
codec_ctx = avcodec_alloc_context3(codec);
if (!codec_ctx) {
fprintf(stderr, "Could not allocate audio codec context\n");
exit(1);
}
// 将解码器和解码器上下文进行关联
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
fprintf(stderr, "Could not open codec\n");
exit(1);
}
// 打开输入文件
infile = fopen(filename, "rb");
if (!infile) {
fprintf(stderr, "Could not open %s\n", filename);
exit(1);
}
// 打开输出文件
outfile = fopen(outfilename, "wb");
if (!outfile) {
av_free(codec_ctx);
exit(1);
}
// 读取文件进行解码
data = inbuf;
data_size = fread(inbuf, 1, AUDIO_INBUF_SIZE, infile);
while (data_size > 0)
{
if (!decoded_frame)
{
if (!(decoded_frame = av_frame_alloc()))
{
fprintf(stderr, "Could not allocate audio frame\n");
exit(1);
}
}
ret = av_parser_parse2(parser, codec_ctx, &pkt->data, &pkt->size,
data, data_size,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0);
if (ret < 0)
{
fprintf(stderr, "Error while parsing\n");
exit(1);
}
data += ret; // 跳过已经解析的数据
data_size -= ret; // 对应的缓存大小也做相应减小
if (pkt->size)
decode(codec_ctx, pkt, decoded_frame, outfile);
if (data_size < AUDIO_REFILL_THRESH) // 如果数据少了则再次读取
{
memmove(inbuf, data, data_size); // 把之前剩的数据拷贝到buffer的起始位置
data = inbuf;
// 读取数据 长度: AUDIO_INBUF_SIZE - data_size
len = fread(data + data_size, 1, AUDIO_INBUF_SIZE - data_size, infile);
if (len > 0)
data_size += len;
}
}
/* 冲刷解码器 */
pkt->data = NULL; // 让其进入drain mode
pkt->size = 0;
decode(codec_ctx, pkt, decoded_frame, outfile);
fclose(outfile);
fclose(infile);
avcodec_free_context(&codec_ctx);
av_parser_close(parser);
av_frame_free(&decoded_frame);
av_packet_free(&pkt);
printf("main finish, please enter Enter and exit\n");
return 0;
}
更多资料:https://github.com/0voice