第十章:ffmpeg和QT开发播放器之音频库使用

写在前面:

    这部分对应视频课程中的5-1~5-8。实现了播放器的音频播放功能。

1、QT音频库介绍

       前面的课程教了如何播放视频,但对音频没有处理。

       需要播放音频的话,要生成一个对象QAudioOutput *out同时要包含它的头文件

       #include <QAudioOutput>,如何设定音频的参数?那么就需要在设置一个QAudioFormat fmt;通过它来指定音频输出的格式。

       fmt.setSampleRate(48000)    :表示一秒钟采集48000个音频数据,越大效果越好。
       fmt.setSampleSize(16)               :表示每个声音数据的大小(4、8、16)位,也就是样本大小。
       fmt.setChannelCount(2)            :表示双声道。

       fmt.setCodec("audio/pcm")       :表示音频格式,这里的pcm格式是未压缩普通格式。

       调用out = new QAudioOutput(fmt);创建输出流。打开音频输出之后,

       之后启动,调用start函数,回返回一个QIODevice 的接口。QIODevice *ad out->start();

       通过QIODevice 接口就可以向里面写入东西播放音频了ad->write(***);

2、封装音频播放类

                      

       首先将构造函数放进protected中,这样做的目的是音频只有一个播放类,所以使用protected,这样就不能随便创建XAudioPlay();构造函数了。只能由内部自己创建(单件模式)

       使用一种方法,隐藏一些信息,把类分装完成之后,使得调用接口的人不知道这个函数是如何实现的,这样将来在换其他方式音频播放时候,调用者就不需要在关注这些变化。所以将这些函数定义成纯虚函数,在继承类中实现这些接口。

       这样的话,我们就将CXAudioPlay做成了继承类。这样将来实现的方法就放在了继承类里面。CXAudioPlay则是我们的接口类就不用实现这些函数功能了。

       如果没有完全实现纯虚函数,就会变成一个抽象类,抽象类是不能实例化的,也就是不能实现对象。

class XAudioPlay
{
public:
	XAudioPlay *Get();
	virtual bool Start() = 0;
	virtual void Play(bool isplay) = 0;
	virtual bool Write(const char *data, int datasize) = 0;

	virtual ~XAudioPlay();
protected:
	XAudioPlay();
};

class CXAudioPlay : public XAudioPlay
{
public:
	bool Start()
	{
		return true;
	}
	void Play(bool isplay)
	{
		
	}
	bool Write(const char *data, int datasize)
	{
		return true;
	}
};

3、实现音频播放的启动和停止接口

       启动的实现,流程大致是设置一个QAudioFormat参数,然后传入参数并start,start之后会返回一个QIODevice。
       每次重新打开一个视频的时候,需要stop,清除output。

void Stop()
{
	if (output)
	{
		output->stop();
		delete output;
		output = NULL;
	}
}

4、实现音频播放的暂停

void Play(bool isplay)
{
	if (!output) return;
	if (isplay)
	{
		output->resume();//恢复播放
	}
	else
	{
		output->suspend();//暂停
	}
}

5、实现音频播放缓冲接口的实现

       write函数的作用,由文件当中读取出音频数据来,在放到QAudio进行播放,但是播放是从缓冲区里面去读的,所以要判断缓冲区的大小。

int GetFree()
{
	if (!output) return 0;
	return output->bytesFree();//获取是否有足够的空间
}

bool Write(const char *data, int datasize)
{
	if (io)
		io->write(data, datasize);
	return true;
}

6、ffmpeg音频解码器打开

       对音频进行打开解码器,进行解码,重采样三步操作。

if (enc->codec_type == AVMEDIA_TYPE_AUDIO)//然后是音频流
	{
			audioStream = i;
			AVCodec *codec = avcodec_find_decoder(enc->codec_id); //判断系统有没有这个音频解码器
			if (avcodec_open2(enc, codec, NULL) < 0)//打开失败
			{
				mutex.unlock();
				return false;
			}
			this->sampleRate = enc->sample_rate;			
			this->channel = enc->channels;
			switch (enc->sample_fmt)
			{
				case AV_SAMPLE_FMT_S16:
					this->sampleSize = 16;
					break;
				case AV_SAMPLE_FMT_S32:
					this->sampleSize = 32;
					break;
				default:
					break;
			}
			printf("audio sample rate:%d sample size %d sample channel %d\n",
				this->sampleRate, this->sampleSize, this->channel);
}

7、音频解码

       在ffmpeg老版本中的视频解码音频解码是用不同函数的,新版的则是使用同一个函数。但是这里会存在一个问题,视频和音频的解码后的数据存放在哪里?肯定不能存放在同个地方。因为视频和音频是两个不同线程,而且要同步播放,用同块内存肯定要出问题的。所以要给音频增加一个内存。

       AVFrame *pcm = NULL;

       加入音频之后还带来一个问题,我们在控制播放进度的时候,以前是以视频为准的 ,控制视频的播放速度,但音频则不行,会导致声音失真。那么之前的pts是存放视频的,现在就要改成存放音频的pts。同时改写之前的AVFrame *Decode(const AVPacket *pkt);函数让它直接返回pts

8、音频重采样

       音频解码之后可以直接播放,但更多时候,解码出来的音频格式并不一定满足当前机器播放的要求。这里需要重采样。例如32位改成16位的或者是通道数2改1.

       引用头文件#include <libswresample/swresample.h>

       再引用重采样的库文件#pragma comment(lib,"swresample.lib")。

       音频解码完之后,如果同段声音放两次会出现问题,所以要保证解码函数和重采样函数在同个线程就好了。

       SwrContext *aCtx = NULL; //创建音频转码器的容器
struct SwrContext *swr_alloc_set_opts	//设置属性
(struct SwrContext *s,					//
int64_t out_ch_layout, 					//表示输出这个通道类型,比如2.1声道、5.1声道这种
enum AVSampleFormat out_sample_fmt, 	//表示输出的采样大小,
int out_sample_rate,					//表示输出的采样率
int64_t  in_ch_layout, 					//表示输入的通道类型
enum AVSampleFormat  in_sample_fmt, 	//表示输入的采样大小
int  in_sample_rate,						//表示输入的采样率
int log_offset, 							//表示偏移
void *log_ctx							//暂时不了解
);

       ffmpeg的音频处理申请空间函数struct SwrContext *swr_alloc(void);;
       ffmpeg的音频处理释放空间函数void swr_free(struct SwrContext **s);;

int swr_convert(		//设置输出空间
struct SwrContext *s,	//打开的重采样设置
uint8_t **out,			//输出的地方
int out_count,			//输出的采样数量的大小,要保证足够大
const uint8_t **in , 		//输入的数据
int in_count			//输入的数据大小
);
//返回值:每一个通道的样本数量,如果小于0就是失败了。

       外部调用该函数的地方是想知道重采样了多少字节使用av_samples_get_buffer_size函数实现

int av_samples_get_buffer_size(
int *linesize,
int nb_channels,					//样本通道
int nb_samples,					//样本数量
enum AVSampleFormat sample_fmt, 	//样本的类型
int align							//
);
int XFFmpeg::ToPCM(char *out)
{
	mutex.lock();

	if (!ic || !pcm || !out)
	{
		mutex.unlock();
		return 0;
	}
	AVCodecContext *ctx = ic->streams[audioStream]->codec;
	if (aCtx == NULL)
	{
		aCtx = swr_alloc();	
		swr_alloc_set_opts(aCtx, ctx->channel_layout,		//设置属性
			AV_SAMPLE_FMT_S16,
			ctx->sample_rate, ctx->channels,
			ctx->sample_fmt,
			ctx->sample_rate,
			0,0);
		swr_init(aCtx);	//初始化
	}
	
	uint8_t *data[1];
	data[0] = (uint8_t *)out;
		int len = swr_convert(aCtx, data, 10000, 
		(const uint8_t **)pcm->data,pcm->nb_samples);//设置输出空间
	if (len <= 0)
	{
		mutex.unlock();
		return 0;
	}
	int outsize = av_samples_get_buffer_size(NULL,ctx->channels, pcm->nb_samples,
		AV_SAMPLE_FMT_S16,0);
	mutex.unlock();

	return outsize;
}

8、完成音视频播放

       首先记得对音频处理函数做互斥。

       音频读取速率也需要控制,也不能够一直读取音频数据来解码。

       音频缓冲区:音频在播放的时候会不断从缓冲区里面读取,读一帧在删除一帧,如果空间满的时候,就没办法写入了,这样就会造成声音的失真。

int free = XAudioPlay::Get()->GetFree();//读取音频缓冲区的剩余空间
if (free < 10000)		//空间不足的话 就不要往缓冲区里面写数据了。
{
	msleep(1);
	continue;
}
	if (pkt.stream_index == XFFmpeg::Get()->audioStream)//如果是音频流,
	{
		XFFmpeg::Get()->Decode(&pkt);//解码音频,Decode内部做了音视频的判断
		av_packet_unref(&pkt);//释放空间
		int len = XFFmpeg::Get()->ToPCM(acm_out);//重采样
		XAudioPlay::Get()->Write(acm_out,len);		//播放音频
		continue;
	}

9、通过多线程和缓冲队列实现音频同步

       目前音视频播放都挺正常的,但是如果播放网络流视频,比如rtsp、rtmp这类网络流的,就很有可能造成不同步现象,

       解决方法:现将视频缓存起来,判断视频和音频是否同步,不同步就等一下再播放。

       使用std的list来缓存视频。

       using namespace std;

       static list<AVPacket> videos;

       这里不能直接拿音频和视频的pts做比较,因为他们两个的timebase不同,所以必须都转换成毫秒才能进行对比。
videos.push_back(pkt);	//入栈
videos.front();//出栈
while (videos.size() > 0)
{
	AVPacket pack =  videos.front();//出栈
	int pts = XFFmpeg::Get()->GetPts(&pack);//获取视频的毫秒数
	if (pts > apts)		//当视频播放进度小于音频的时候才解码
	{
		break;
	}
	XFFmpeg::Get()->Decode(&pack);
	av_packet_unref(&pack);//释放空间
	videos.pop_front();
}





猜你喜欢

转载自blog.csdn.net/tainjau/article/details/79875004