播放器二(FFMPEG+SDL+AUDIO)


AUDIO大致流程:

//初始化输入
avformat_open_input()           -- 打开对应音频文件
avformat_find_stream_info()     -- 从输入文件中获取到流的相关信息,例如:文件中流的数量

//初始化解码器
avcodec_find_decoder()          -- 根据ffmpeg提供的解码器id,找到对应的解码器
avcodec_open2()                     -- 打开解码器

//初始化输出
avformat_alloc_context()        -- 创建输出信息上下文
avcodec_find_encoder()          -- 找到编码器
avcodec_alloc_context3()            -- 根据编码器,初始化编码上下文
avcodec_open2()                 -- 打开编码器

//初始化一个FIFO(先进先出)
av_audio_fifo_alloc()           -- 初始化一个先进先出的缓存,用了存储解码后的pcm数据

//初始化音频重采用器
swr_alloc_set_opts()            -- 设置转化器参数
swr_init()                      -- 初始化转换器

//开始音频转换

while(finished) {
// 解码
av_read_frame()                     -- 读取要进行转码的数据
avcodec_decode_audio4()         -- 进行解码
av_samples_alloc()              -- 创建样本空间
swr_convert()                       -- 数据重采样
av_audio_fifo_write()           -- 将数据存储到fifo缓存中

// 编码
av_audio_fifo_read()            -- 从fifo缓存中读取pcm数据
avcodec_encode_audio2()         -- 将数据进行编码

}

1、SDL

现在我们要来播放声音。SDL 也为我们准备了输出声音的方法。函数 SDL_OpenAudio()本身就是用
来打开声音设备的。它使用一个叫做 SDL_AudioSpec 结构体作为参数,这个结构体中包含了我们将要输
出的音频的所有信息。
在我们展示如何建立之前,让我们先解释一下电脑是如何处理音频的。数字音频是由一长串的样本流
组成的。每个样本表示声音波形中的一个值。声音按照一个特定 的采样率来进行录制,采样率表示以多
快的速度来播放这段样本流,它的表示方式为每秒多少次采样。例如 22050 和 44100 的采样率就是电台
和 CD 常用的 采样率。此外,大多音频有不只一个通道来表示立体声或者环绕。例如,如果采样是立体
声,那么每次的采样数就为 2 个。当我们从一个电影文件中等到数据的时 候,我们不知道我们将得到多
少个样本,但是 ffmpeg 将不会给我们部分的样本――这意味着它将不会把立体声分割开来。
SDL 播放声音的方式是这样的:你先设置声音的选项:采样率(在 SDL 的结构体中被叫做 freq 的表
示频率 frequency),声音通道数和其它的参 数,然后我们设置一个回调函数和一些用户数据 userdata。
当开始播放音频的时候,SDL 将不断地调用这个回调函数并且要求它来向声音缓冲填入一个特 定的数量
的字节。当我们把这些信息放到 SDL_AudioSpec 结构体中后,我们调用函数 SDL_OpenAudio()就会打开
声音设备并且给我们送 回另外一个 AudioSpec 结构体。这个结构体是我们实际上用到的--因为我们不
能保证得到我们所要求的

2、核心转换函数

int  swr_convert (struct SwrContext *suint8_t **outint out_count, const uint8_t **inint in_count)
转换音频(注意初始化)
in和in_count可以设置为0,以最后刷新最后几个样本。
如果提供比输出空间更多的输入,则输入将被缓冲。 您可以通过使用swr_get_out_samples()来检索给定数量的输入样本所需的输出样本数的上限来避免此缓冲。 转换将直接运行,无需复制即可。
参数:s:分配Swr上下文,并设置参数
   out:输出缓冲区,只有在打包音频的情况下才需要设置第一个
    out_count:每个通道样品中可用于输出的空间量
   in:输入缓冲区,只有在打包音频的情况下才需要设置第一个
   in_count:在一个通道中可用的输入样本数
返回:每个通道的采样数量,误差的负值


3、SDL回调

 fill_auido()当中将audio_pos叠加在audio预设的数据stream,就是混播音频的关键,其次auido_len确保了有序

示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define __STDC_CONSTANT_MACROS

#ifdef _WIN32
//Windows
extern "C"
{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswresample/swresample.h"
#include "SDL2/SDL.h"
};
#else
//Linux...
#ifdef __cplusplus
extern "C"
{
#endif
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
#ifdef __cplusplus
};
#endif
#endif


/*
*SDL播放声音的基本流程如下: 
*(1)创建一个回调函数用于混合音频数据,并放入音频流; 
*(2)设置音频参数,调用SDL_OpenAudio,打开音频设备; 
*(3)调用SDL_PauseAudio(0),进行音频回放; 
*(4)回放结束后,调用SDL_CloseAudio() 关闭音频设备。
*/

#define MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio

//Output PCM
#define OUTPUT_PCM 1
//Use SDL
#define USE_SDL 1

static Uint8 *audio_chunk;
static Uint32 audio_len;
static Uint8 *audio_pos;

//回调函数用于混合音频数据,并放入音频流
void fill_audio(void *udata,Uint8 *stream,int len){

	//printf("------------>audio_len == %d\n",audio_len);
	SDL_memset(stream,0,len);
	if(audio_len == 0) return ;

	len = (len > audio_len ? audio_len : len);
	//在fill_audio()当中将每个数据叠加在audio预设的数据流stream,就是混播音频的关键。
	SDL_MixAudio(stream,audio_pos,len,SDL_MIX_MAXVOLUME);
	audio_pos += len;
	audio_len -= len;
	//printf("------------>audio_len == %d\n",audio_len);
}

int main(int argc,char* argv[]){

	//以下参数跟视频播放一样
	AVFormatContext *pFormatCtx;
	int  i, audioStream;
	AVCodecContext *pCodecCtx;
	AVCodec        *pCodec;
	AVPacket        *packet;
	uint8_t         *out_buffer;
	AVFrame         *pFrame;
	
	//如下介绍
	SDL_AudioSpec   wanted_spec;

	int ret;
	uint32_t len = 0;
	int got_picture;
	int index = 0;
	int64_t in_channel_layout;
	struct SwrContext *au_convert_ctx;

	FILE *pFile = NULL;
	char url[] = "WavinFlag.aac";

	av_register_all();

	pFormatCtx = avformat_alloc_context();


	if(avformat_open_input(&pFormatCtx,url,NULL,NULL) != 0){
		printf("Could not open input stream\n");
		return -1;
	}

	if(avformat_find_stream_info(pFormatCtx,NULL) < 0){
		printf("Could not find stream infomation.\n");
		return -1;
	}

	av_dump_format(pFormatCtx,0,url,false);



	audioStream = -1;

	for(i = 0;i < pFormatCtx->nb_streams;i++){
		if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO){
			audioStream = i;
			break;
		}
	}

	if(audioStream == -1){
		printf("Do not find a audio stream \n");
		return -1;
	}

	pCodecCtx = pFormatCtx->streams[audioStream]->codec;

	pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
	if(NULL == pCodec){
		printf("Codec not find.\n");
		return -1;
	}

	if(avcodec_open2(pCodecCtx,pCodec,NULL)<0){
		printf("Could not open codec.\n");
		return -1;
	}

#if OUTPUT_PCM
	pFile = fopen("output.pcm","wb");
#endif
	packet = (AVPacket*)av_malloc(sizeof(AVPacket));
	av_init_packet(packet);

	//设置Audio参数
	uint64_t out_channel_layout = AV_CH_LAYOUT_STEREO;
	int out_nb_samples = pCodecCtx->frame_size;
	AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
	int out_sample_rate = 44100;
	int out_channels = av_get_channel_layout_nb_channels(out_channel_layout);
	
	//存放解码后的数据用于写pcm文件
	int out_buffer_size = av_samples_get_buffer_size(NULL,out_channels,out_nb_samples,out_sample_fmt,1);
	out_buffer = (uint8_t *)av_malloc(MAX_AUDIO_FRAME_SIZE*2);
	//存放解码后数据结构
	pFrame = av_frame_alloc();

	//SDL
#if USE_SDL
	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
		printf("SDL Init failed.\n");
		return -1;
	}

	//SDL_AudioSpec
	/*freq: DSP 频率 (每秒采样); 
	*format:音频数据格式;
	*channels:声道数: 1 单声道, 2 立体声;
	*silence:audio buffer silence value (计算得);
	*samples:audio buffer size in samples (power of 2);
	*size:音频缓存区大小(字节数)(计算得);
	*callback:当音频设备需要更多数据时调用的回调函数;
	*userdata:传递给 callback 函数的第一个参数
	*/
	wanted_spec.freq = out_sample_rate;
	wanted_spec.format = AUDIO_S16SYS;
	wanted_spec.channels = out_channels;
	wanted_spec.samples = out_nb_samples;
	wanted_spec.callback = fill_audio;
	wanted_spec.userdata = pCodecCtx;
	//打开音频设备
	if(SDL_OpenAudio(&wanted_spec,NULL)<0){
		printf("can not open audio.\n");
		return -1;
	}
	printf("Bitrate:\t %3d\n", pFormatCtx->bit_rate);
	printf("Decoder Name:\t %s\n", pCodecCtx->codec->long_name);
	printf("Channels:\t %d\n", pCodecCtx->channels);
	printf("Sample per Second\t %d \n", pCodecCtx->sample_rate);
#endif
	/** 
	* 接下来判断我们之前设置SDL时设置的声音格式(AV_SAMPLE_FMT_S16),声道布局, 
	* 采样频率,每个AVFrame的每个声道采样数与 
	* 得到的该AVFrame分别是否相同,如有任意不同,我们就需要swr_convert该AvFrame, 
	* 然后才能符合之前设置好的SDL的需要,才能播放 
	*/ 
	//通过av_get_channel_layout_nb_channels()和av_get_default_channel_layout()这些函数可以得到channels和channellayout的转换。
	in_channel_layout = av_get_default_channel_layout(pCodecCtx->channels);
	
	//初始化音频重采用器
	au_convert_ctx = swr_alloc();
	au_convert_ctx = swr_alloc_set_opts(au_convert_ctx,out_channel_layout,out_sample_fmt,out_sample_rate,
		in_channel_layout,pCodecCtx->sample_fmt,pCodecCtx->sample_rate,0,NULL);
	swr_init(au_convert_ctx);

	//用此函数来暂停播放,或播放。根据参数来决定,如果参数是非0值就暂停,如果是0值就播放
	SDL_PauseAudio(0);

	while(av_read_frame(pFormatCtx,packet)>=0){

		if(audioStream == packet->stream_index){
			//使用avcodec_decode_audio4函数来解码音频
			ret = avcodec_decode_audio4(pCodecCtx,pFrame,&got_picture,packet);
			if(0 > ret){
				printf("Error in decoding audio frame.\n");
				return -1;
			}


			if(0 < got_picture){
				//数据重采样提供给SDL播放
				/*由于ffmpeg最新版本(从2.1开始貌似)使用avcodec_decode_audio4函数来解码音频,
				*但解码得到的数据类型为float 4bit,而播放器播放的格式一般为S16(signed 16bit)
				*,就需要对解码得到的数据进行转换,然而,ffmpeg已经帮我们做好了,
				*只需调用API就可以了,这个函数就是:swr_convert
				*out :输出缓冲区,只有第一个需要设置的情况下,包装音频
				*out_count :每通道可用输出样本的数量
				*in:输入缓冲区,只有第一个需要设置的情况下,包装音频,解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
				*in_count :每通道可用输入样本的数量(音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个)
				*/
				swr_convert(au_convert_ctx,&out_buffer,MAX_AUDIO_FRAME_SIZE,(const uint8_t**)pFrame->data,pFrame->nb_samples);
				//printf("index : %5d\t pts : %lld\t packet size : %d\n",index,packet->pts,packet->size);


#if OUTPUT_PCM
				fwrite(out_buffer,1,out_buffer_size,pFile);

#endif
				index ++;


			}

#if USE_SDL
			while(audio_len > 0){
				//注意audio_len的用法
				//保证fill_audio回调有序的播放每一帧数据
				//上一帧SDL_MixAudio之后auido_len = 0,跳出塞下一帧
				SDL_Delay(1);
			}

			//每次循环的时候都更新数据的buffer、pos等信息,
			//在每次SDL_PauseAudio回调的时候都能够播放下一时刻的数据
			//(注意 这里必须把每一个PCM文件的数据都进行更新)。
			audio_chunk = (Uint8 *)out_buffer;

			audio_len = out_buffer_size;
			audio_pos = audio_chunk;
#endif

		}
		av_free_packet(packet);
	}


	swr_free(&au_convert_ctx);

#if USE_SDL
	SDL_CloseAudio();
	SDL_Quit();
#endif


#if OUTPUT_PCM
	fclose(pFile);
#endif

	av_free(out_buffer);
	avcodec_close(pCodecCtx);
	avformat_close_input(&pFormatCtx);

	return 0;


}





猜你喜欢

转载自blog.csdn.net/yuanchunsi/article/details/79570650