1. ffmpeg 视频解码一
2. ffmpeg 视频解码二
3. ffmpeg 音频解码一
前言
前面已经介绍了视频的解码流程,这篇开始就开始音频解码了,同样是两篇,一篇使用parser解析器做解析,一篇按常规流程处理。
一些基础知识
-
采样率(sample_rate):
即取样频率,定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。采样频率的倒数是采样周期或者叫作采样时间,它是采样之间的时间间隔。通俗的讲采样频率是指计算机每秒钟采集多少个信号样本。
-
采样数(frame_size):
一帧音频的大小。
-
采样格式(sample_fmt):
音频sample的存储格式。
可以使用8位无符号整数、16位有符号整数、32位有符号整数以及单精度浮点数,双精度浮点数表示一个采样。但是,没有使用24位的有符号整数,这是因为这些不同的格式使用的是原生的C类型,而C中是没有24位的长度的类型的。
我们可以使用以下命令查看ffmpeg支持的格式:
ffplay -sample_fmts
当然也可查看源码,这里贴出 SampleFmtInfo(包含AVSampleFormat相关转化的信息)结构体的源代码:
static const SampleFmtInfo sample_fmt_info[AV_SAMPLE_FMT_NB] = { [AV_SAMPLE_FMT_U8] = { .name = "u8", .bits = 8, .planar = 0, .altform = AV_SAMPLE_FMT_U8P }, [AV_SAMPLE_FMT_S16] = { .name = "s16", .bits = 16, .planar = 0, .altform = AV_SAMPLE_FMT_S16P }, [AV_SAMPLE_FMT_S32] = { .name = "s32", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_S32P }, [AV_SAMPLE_FMT_S64] = { .name = "s64", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_S64P }, [AV_SAMPLE_FMT_FLT] = { .name = "flt", .bits = 32, .planar = 0, .altform = AV_SAMPLE_FMT_FLTP }, [AV_SAMPLE_FMT_DBL] = { .name = "dbl", .bits = 64, .planar = 0, .altform = AV_SAMPLE_FMT_DBLP }, [AV_SAMPLE_FMT_U8P] = { .name = "u8p", .bits = 8, .planar = 1, .altform = AV_SAMPLE_FMT_U8 }, [AV_SAMPLE_FMT_S16P] = { .name = "s16p", .bits = 16, .planar = 1, .altform = AV_SAMPLE_FMT_S16 }, [AV_SAMPLE_FMT_S32P] = { .name = "s32p", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_S32 }, [AV_SAMPLE_FMT_S64P] = { .name = "s64p", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_S64 }, [AV_SAMPLE_FMT_FLTP] = { .name = "fltp", .bits = 32, .planar = 1, .altform = AV_SAMPLE_FMT_FLT }, [AV_SAMPLE_FMT_DBLP] = { .name = "dblp", .bits = 64, .planar = 1, .altform = AV_SAMPLE_FMT_DBL }, };
其中name为格式名称,bits是在计算机中所占的位数,plannar是文件存储方式,altform是获取文件根据存储方式不同时相应的名称(例:u8 是 plannar=0 的格式 ,转换为 plannar=1 时 即是 u8p)。
sample有两种类型的存储方式:平面(planar)和打包(packed),在planar中每一个通道独自占用一个存储平面;在packed中,所有通道的sample交织存储在同一个平面。
-
声道信息:
channels 为 音频的 通道数 1 2 3 4 5…
channel_layout 为音频 通道格式类型 如 单通道 双通道 …对于单声道声音文件,采样数据为八位的短整数(short int 00H-FFH);
而对于双声道立体声声音文件,每次采样数据为一个16位的整数(int),高八位(左声道)和低八位(右声道)分别代表两个声道。
如果是双声道(stereo),采样就是双份的,文件也差不多要大一倍。
音频信息
如果音频,样本:fltp;采样率:44100;声道:2。
av_get_bytes_per_sample(fltp) == 4;
-
AAC(nb_samples和frame_size = 1024)
则可以得到一帧音频的大小为:
4 * 2 * 1024 = 8192(字节)
一帧的播放时间是
1024*1000000/44100= 46.43ms -
MP3(nb_samples和frame_size = 1152)
则可以得到一帧音频的大小为:
4 * 2 * 1152= 9216(字节)
一帧的播放时间是
1152*1000000/44100= 52.24ms
流程图
代码流程即如流程图所示,下面讲解一下当中部分函数的作用。
-
av_parser_init
这是一个解析器,我们根据解码器,实例化这个解析器,后面解析数据时使用。 -
av_parser_parse2
我们从输入文件得到的原始数据(不适用ffmpeg自带的api的话),直接使用是不行的,此时我们就需要把这个原始数据使用上面实例化的解析器来解析,把数据分割成帧,为后面解码数据做准备。 -
avcodec_send_packet
发送我们刚刚得到的解析数据到解码器做解码。 -
avcodec_receive_frame
获取解码之后的数据。
源码
#pragma once
#define __STDC_CONSTANT_MACROS
#define _CRT_SECURE_NO_WARNINGS
extern "C"
{
#include "libavcodec/avcodec.h"
}
//缓冲区大小(缓存5帧数据)
#define AUDIO_INBUF_SIZE 40960
/*
name depth
u8 8
s16 16
s32 32
flt 32
dbl 64
u8p 8
s16p 16
s32p 32
fltp 32
dblp 64
s64 64
s64p 64
//此代码解码的音频文件格式如下:
//AAC文件(一帧1024字节),双声道(2),FLTP(32位,4字节)
//AAC文件 frame_size 和 nb_samples 大小均为1024
//一帧音频所占字节大小
//1024*2*4=8192字节
*/
#define AUDIO_REFILL_THRESH 8192
using namespace std;
#define INPUT_FILE_NAME "lh_online.aac"
#define OUTPUT_FILE_NAME "lh_online.pcm"
static int get_format_from_sample_fmt(const char** fmt,
enum AVSampleFormat sample_fmt)
{
int i;
struct sample_fmt_entry {
enum AVSampleFormat sample_fmt; const char* fmt_be, * fmt_le;
} sample_fmt_entries[] = {
{ AV_SAMPLE_FMT_U8, "u8", "u8" },
{ AV_SAMPLE_FMT_S16, "s16be", "s16le" },
{ AV_SAMPLE_FMT_S32, "s32be", "s32le" },
{ AV_SAMPLE_FMT_FLT, "f32be", "f32le" },
{ AV_SAMPLE_FMT_DBL, "f64be", "f64le" },
};
*fmt = NULL;
for (i = 0; i < FF_ARRAY_ELEMS(sample_fmt_entries); i++) {
struct sample_fmt_entry* entry = &sample_fmt_entries[i];
if (sample_fmt == entry->sample_fmt) {
*fmt = AV_NE(entry->fmt_be, entry->fmt_le);
return 0;
}
}
av_log(NULL, AV_LOG_ERROR, "sample format %s is not supported as output format\n", av_get_sample_fmt_name(sample_fmt));
return -1;
}
static void decode(AVCodecContext* dec_ctx, AVFrame* frame, AVPacket* pkt,
FILE* ofile)
{
int i, ch;
int ret, data_size;
ret = avcodec_send_packet(dec_ctx, pkt);
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "发送数据包到解码器出错。\n");
exit(1);
}
while (ret >= 0) {
ret = avcodec_receive_frame(dec_ctx, frame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
return;
else if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Error sending a packet for decoding.\n");
exit(1);
}
printf("frame_number: %d \n", dec_ctx->frame_number);
//获取每个采样点当中每个声道的大小
data_size = av_get_bytes_per_sample(dec_ctx->sample_fmt);
if (data_size < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to calculate data size.\n");
exit(1);
}
//遍历采样点
for (i = 0; i < frame->nb_samples; i++) {
//遍历声道
for (ch = 0; ch < dec_ctx->channels; ch++) {
fwrite(frame->data[ch] + data_size * i, 1, data_size, ofile);
}
}
}
}
int main(int argc, char* argv[])
{
const AVCodec* codec;
AVCodecParserContext* parser;
AVCodecContext* c = NULL;
FILE* ifile, * ofile;
AVFrame* frame;
AVPacket* pkt;
uint8_t inbuf[AUDIO_INBUF_SIZE + AV_INPUT_BUFFER_PADDING_SIZE];
uint8_t* data;
size_t data_size;
int ret,len;
enum AVSampleFormat sfmt;
const char* fmt;
//初始化inbuf数字默认值
memset(inbuf + AUDIO_INBUF_SIZE, 0, AV_INPUT_BUFFER_PADDING_SIZE);
//获取解码器(此处需要读取的文件是AAC,故)
codec = avcodec_find_decoder(AV_CODEC_ID_AAC);
if (!codec) {
av_log(NULL, AV_LOG_ERROR, "Codec not found.\n");
exit(1);
}
//注册解析器
parser = av_parser_init(codec->id);
if (!parser) {
av_log(NULL, AV_LOG_ERROR, "parser not found.\n");
exit(1);
}
//分配解析器上下文
c = avcodec_alloc_context3(codec);
if (!c) {
av_log(NULL, AV_LOG_ERROR, "Could not allocate video codec context.\n");
exit(1);
}
//打开解码器
if (avcodec_open2(c, codec, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not open codec.\n");
exit(1);
}
//分配AVPacket
pkt = av_packet_alloc();
if (!pkt) {
exit(1);
}
//分配AVFrame
frame = av_frame_alloc();
if (!frame) {
exit(1);
}
//打开输入文件
ifile = fopen(INPUT_FILE_NAME, "rb");
if (!ifile) {
av_log(NULL, AV_LOG_ERROR, "Could not open \s.\n", INPUT_FILE_NAME);
exit(1);
}
//打开输入文件
ofile = fopen(OUTPUT_FILE_NAME, "wb+");
if (!ofile) {
av_log(NULL, AV_LOG_ERROR, "Could not open \s.\n", OUTPUT_FILE_NAME);
exit(1);
}
//从输入流 ifile 读取数据到 inbuf 所指向的数组中
data = inbuf;
data_size = fread(inbuf, 1, AUDIO_INBUF_SIZE, ifile);
while (data_size > 0) {
//使用注册的解析器 parser 把数据分割成帧
ret = av_parser_parse2(parser, c, &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(c, frame, pkt, ofile);
//判断缓存区剩余数据是否小于一帧音频大小
//小于的话从文件继续读取,之后在送往解码
if (data_size < AUDIO_REFILL_THRESH) {
memmove(inbuf, data, data_size);
data = inbuf;
len = fread(data + data_size, 1,
AUDIO_INBUF_SIZE - data_size, ifile);
if (len > 0)
data_size += len;
}
}
//flush 解码器
decode(c, frame, NULL, ofile);
//此时就已经解码完了,我们稍后使用ffplay播放下音频
//解码出来的pcm数据是没有这些基础数据的,我们需要从元数据获取
//打印下基本信息
//声道数
printf("channels: %d \n", c->channels);
//采样率
printf("sample_rate: %d \n", c->sample_rate);
//一帧音频所占字节代销
printf("buffer: %d \n", av_samples_get_buffer_size(NULL, c->channels, c->frame_size, c->sample_fmt, 1));
//采样格式
sfmt = c->sample_fmt;
printf("sample_fmt: %s \n", av_get_sample_fmt_name(sfmt));
//如果为planar,转换为packed格式
if (av_sample_fmt_is_planar(sfmt)) {
const char* packed = av_get_sample_fmt_name(sfmt);
sfmt = av_get_packed_sample_fmt(sfmt);
}
if (get_format_from_sample_fmt(&fmt, sfmt) < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not get forma \s.\n", av_get_sample_fmt_name(sfmt));
exit(1);
}
//打印播放命令
printf("Play the output audio file with the command:\n"
"ffplay -f %s -ac %d -ar %d %s\n",
fmt, c->channels, c->sample_rate,OUTPUT_FILE_NAME);
//资源释放
fclose(ifile);
fclose(ofile);
av_parser_close(parser);
avcodec_free_context(&c);
av_frame_free(&frame);
av_packet_free(&pkt);
return 0;
}
此实例演示了一个将aac文件解码成pcm文件的流程。
打印信息如下:
可见待解码文件是一个 有2个声道,采样率为44100HZ,采样格式为fltp的文件,共有1478帧。
接下来使用命令播放我们解码出来的音频试试:
ffplay -f f32le -ac 2 -ar 44100 lh_online.pcm
结果:
此时你应该能听到播放的音频声音,大功告成。
到此,基于parser解析器解码音频的方式就讲述完了。
下一篇和视频一样将讲述纯基于API的方式,应该是比这个方便很多。