写在前面:
这部分对应视频课程中的5-1~5-8。实现了播放器的音频播放功能。前面的课程教了如何播放视频,但对音频没有处理。
需要播放音频的话,要生成一个对象QAudioOutput *out同时要包含它的头文件
#include
fmt.setCodec("audio/pcm") :表示音频格式,这里的pcm格式是未压缩普通格式。
调用out = new QAudioOutput(fmt);创建输出流。打开音频输出之后,
之后启动,调用start函数,回返回一个QIODevice 的接口。QIODevice *ad out->start();通过QIODevice 接口就可以向里面写入东西播放音频了ad->write(***);
首先将构造函数放进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;
}
};
启动的实现,流程大致是设置一个QAudioFormat参数,然后传入参数并start,start之后会返回一个QIODevice。
每次重新打开一个视频的时候,需要stop,清除output。
void Stop()
{
if (output)
{
output->stop();
delete output;
output = NULL;
}
}
void Play(bool isplay)
{
if (!output) return;
if (isplay)
{
output->resume();//恢复播放
}
else
{
output->suspend();//暂停
}
}
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;
}
对音频进行打开解码器,进行解码,重采样三步操作。
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);
}
在ffmpeg老版本中的视频解码音频解码是用不同函数的,新版的则是使用同一个函数。但是这里会存在一个问题,视频和音频的解码后的数据存放在哪里?肯定不能存放在同个地方。因为视频和音频是两个不同线程,而且要同步播放,用同块内存肯定要出问题的。所以要给音频增加一个内存。
AVFrame *pcm = NULL;
加入音频之后还带来一个问题,我们在控制播放进度的时候,以前是以视频为准的 ,控制视频的播放速度,但音频则不行,会导致声音失真。那么之前的pts是存放视频的,现在就要改成存放音频的pts。同时改写之前的AVFrame *Decode(const AVPacket *pkt);函数让它直接返回pts音频解码之后可以直接播放,但更多时候,解码出来的音频格式并不一定满足当前机器播放的要求。这里需要重采样。例如32位改成16位的或者是通道数2改1.
引用头文件#include
再引用重采样的库文件#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;
}
首先记得对音频处理函数做互斥。
音频读取速率也需要控制,也不能够一直读取音频数据来解码。
音频缓冲区:音频在播放的时候会不断从缓冲区里面读取,读一帧在删除一帧,如果空间满的时候,就没办法写入了,这样就会造成声音的失真。
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;
}
目前音视频播放都挺正常的,但是如果播放网络流视频,比如rtsp、rtmp这类网络流的,就很有可能造成不同步现象,
解决方法:现将视频缓存起来,判断视频和音频是否同步,不同步就等一下再播放。
使用std的list来缓存视频。
using namespace std;
static list
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();
}