ffmpeg 音频解码一

1. ffmpeg 视频解码一
2. ffmpeg 视频解码二
3. ffmpeg 音频解码一

前言

前面已经介绍了视频的解码流程,这篇开始就开始音频解码了,同样是两篇,一篇使用parser解析器做解析,一篇按常规流程处理。

一些基础知识

  1. 采样率(sample_rate):

    即取样频率,定义了每秒从连续信号中提取并组成离散信号的采样个数,它用赫兹(Hz)来表示。采样频率的倒数是采样周期或者叫作采样时间,它是采样之间的时间间隔。通俗的讲采样频率是指计算机每秒钟采集多少个信号样本。

  2. 采样数(frame_size):

    一帧音频的大小。

  3. 采样格式(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交织存储在同一个平面。

  4. 声道信息:

    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;

  1. AAC(nb_samples和frame_size = 1024)
    则可以得到一帧音频的大小为:
    4 * 2 * 1024 = 8192(字节)
    一帧的播放时间是
    1024*1000000/44100= 46.43ms

  2. MP3(nb_samples和frame_size = 1152)
    则可以得到一帧音频的大小为:
    4 * 2 * 1152= 9216(字节)
    一帧的播放时间是
    1152*1000000/44100= 52.24ms

流程图

ffmpeg 音频解码一

代码流程即如流程图所示,下面讲解一下当中部分函数的作用。

  1. av_parser_init
    这是一个解析器,我们根据解码器,实例化这个解析器,后面解析数据时使用。
  2. av_parser_parse2
    我们从输入文件得到的原始数据(不适用ffmpeg自带的api的话),直接使用是不行的,此时我们就需要把这个原始数据使用上面实例化的解析器来解析,把数据分割成帧,为后面解码数据做准备。
  3. avcodec_send_packet
    发送我们刚刚得到的解析数据到解码器做解码。
  4. 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文件的流程。
打印信息如下:

ffmpeg 音频解码一
可见待解码文件是一个 有2个声道,采样率为44100HZ,采样格式为fltp的文件,共有1478帧。

接下来使用命令播放我们解码出来的音频试试:

ffplay -f f32le -ac 2 -ar 44100 lh_online.pcm

结果:
ffmpeg 音频解码一
此时你应该能听到播放的音频声音,大功告成。

到此,基于parser解析器解码音频的方式就讲述完了。
下一篇和视频一样将讲述纯基于API的方式,应该是比这个方便很多。

上一篇:Android音视频开发全系列教程


下一篇:OpenWrt R20.10.20 / LuCI Master (git-20.256.12360-1a54222) 不能访问内网网址