需求背景介绍:项目中会有多个pcm音频数据段,每一段是不同人的发言短音频,可以播放其中任何人的音频,当播放其中一个人的音频时,可以通过切换按钮随时切换播放另外一个人的音频,数据量不算太大,十五秒上限的数据量,但是每次播放音频数据时是将某个人一整段的音频一次性给QAudioOutput去播放。当播放某个人的音频结束时返回消息。
了解到需求之后就开始写代码了,写代码之前首先了解一下QAudioOutput的四种状态:
- QAudio::ActiveState:正在播放音频数据时的状态,QAudioOutput调用start()函数以后才可以出现的状态。
- QAudio::IdleState:没有音频数据播发时的状态,QAudioOutput调用start()函数以后才可以出现的状态,与QAudio::ActiveState状态相对应。
- QAudio::StoppedState:调用stop()函数以后的一种状态,音频设备被关闭,此后不能再播放音频。
- QAudio::SuspendedState:调用暂停函数suspend()后的一种状态,此时的音频数据仍在QAudioOutput的缓存中,可以通过resume()继续播放缓存中的音频。
头文件如下:
#ifndef PLAYPCMAUDIO_H
#define PLAYPCMAUDIO_H
#include <QObject>
#include <QAudioFormat>
#include <QAudioOutput>
#include <QSharedPointer>
class PCMPlayer:public QObject
{
Q_OBJECT
signals:
void sig_PlayFinished();
public:
PCMPlayer();
~PCMPlayer();
public slots:
//dt:PCM数据,len:数据的长度
void playData(char *dt,int len);
public:
//nChannels声道数,SampleRate采样率,nSampleSize采样深度
bool initData(int nChannels = 1, int SampleRate = 16000, int nSampleSize = 16);
private:
QAudioOutput *m_audioOut = nullptr;
QIODevice *m_device = nullptr;
bool m_bFinished = true;//音频播放完成通过QAudio::IdleState状态判断,但是在调用start()函数后因为还没来得及填充数据,会立刻触发一次QAudio::IdleState状态,该变量为了判断是播放音频结束触发还是start()触发的标识
};
#endif // PLAYPCMAUDIO_H
实现如下:
#include "PCMPlayer.h"
#include <QDebug>
#include <windows.h>
PCMPlayer::PCMPlayer()
{
m_audioOut = nullptr;
m_device = nullptr;
}
PCMPlayer::~PCMPlayer()
{
if(nullptr != m_device){
m_device->close();
}
if(nullptr != m_audioOut){
m_audioOut->stop();
delete m_audioOut;
m_audioOut = nullptr;
}
}
void PCMPlayer::playData(char *dt,int len)
{
m_bFinished = true;//重新播放另一段之前,默认之前的已经播放完成
if(nullptr != m_device){
m_device->write(dt, len);
}
}
bool PCMPlayer::initData(int nChannels,int SampleRate,int nSampleSize)
{
if(nullptr != m_audioOut){
return true;
}
QAudioFormat audioFmt;
audioFmt.setChannelCount(nChannels);
audioFmt.setCodec("audio/pcm");
audioFmt.setSampleRate(SampleRate);
audioFmt.setSampleSize(nSampleSize);
audioFmt.setByteOrder(QAudioFormat::LittleEndian);
audioFmt.setSampleType(QAudioFormat::SignedInt);
m_audioOut = new QAudioOutput(audioFmt);
connect(m_audioOut,&QAudioOutput::stateChanged,this,[&](QAudio::State state){
if (QAudio::ActiveState == state)
{
m_bFinished = false;//新的一段开始播放,设置未播放完的标识位
}
else if(QAudio::IdleState == state)
{
if (!m_bFinished)//与QAudio::ActiveState中标识位的false对应
{
emit sig_PlayFinished();
}
}
});
m_device = m_audioOut->start();
if(nullptr == m_device){
return false;
}
return true;
}
信心满满的开始进行测试了:
PCMPlayer *player = new PCMPlayer();
player->initData();
QFile fil("C:/audio0.pcm");
fil.open(QFile::ReadOnly);
QByteArray byte = fil.readAll();
fil.close();
player->playData(byte.data(),byte.length());
播放了一遍,没有声音。又播放了一遍,还是没有声音,但是调用playData()函数后会紧接着依次进入QAudio::ActiveState状态,QAudio::IdleState状态~
难道是pcm数据格式跟初始化播放的参数有出处?在playData()函数中将写入的数据存到本地文件中。拿Audacity软件进行pcm数据分析,没问题,数据可以正常播放,pcm数据的参数信息与初始化播放的参数一致。
此时只一种可能了,QAudioOutput的初始化不完整,缺少了某个步骤,导致无法播放出声音,帮助手册走起~
忽然看到两个函数,setBufferSize(),bufferSize(),还需要设置缓存大小吗?然后测试了一下,在调用start()函数之前打印bufferSize()值是0。调用start()以后,bufferSize()的值是6400。突然之间恍然大悟,因为之前做过另外一个项目,播放实时音频,一次也就一帧的长度,大小不过几百字节,而一次写入几秒的数据长度远超过6400,如此看来是缓存不够,没有足够的空间存储音频数据,6400之后的数据丢掉导致的,因为最开始的零点几秒是静音的,所以播放不出声音也正常。这也就能解释的清楚,调用playData()函数后会紧接着依次进入QAudio::ActiveState状态,QAudio::IdleState状态的情况了,是因为有播放静音的6400字节数据导致的。
仔细研究了一下setBufferSize()函数,这个函数需要在调用start()函数之前调用,这样才可以起作用,于是在初始化播放的时候加了设置缓存大小的调用就播放出声音来了。代码如下:
bool PCMPlayer::initData(int nChannels,int SampleRate,int nSampleSize)
{
if(nullptr != m_audioOut){
return true;
}
QAudioFormat audioFmt;
audioFmt.setChannelCount(nChannels);
audioFmt.setCodec("audio/pcm");
audioFmt.setSampleRate(SampleRate);
audioFmt.setSampleSize(nSampleSize);
audioFmt.setByteOrder(QAudioFormat::LittleEndian);
audioFmt.setSampleType(QAudioFormat::SignedInt);
m_audioOut = new QAudioOutput(audioFmt);
m_audioOut->setBufferSize(10000000);添加了这一行代码就可以了
connect(m_audioOut,&QAudioOutput::stateChanged,this,[&](QAudio::State state){
if (QAudio::ActiveState == state)
{
m_bFinished = false;//新的一段开始播放,设置未播放完的标识位
}
else if(QAudio::IdleState == state)
{
if (!m_bFinished)//与QAudio::ActiveState中标识位的false对应
{
emit sig_PlayFinished();
}
}
});
m_device = m_audioOut->start();
if(nullptr == m_device){
return false;
}
return true;
}
此时再执行程序,可以播放出声音了,开心😊
后面开始修改测试用例,点击不同的按钮,播放不同的音频,且只要点击立刻播放当前点击按钮对应的音频,停止之前播放的音频。按照现在的接口试了一下。点击第一个按钮,播放第一段音频,紧接着点击第二个按钮,却没有立刻播放第二段音频,而是等第一段音频播放结束后才会播放第二段音频。问题很显然,点击第二个按钮的时候,会将第二段音频写入播放器的缓存中,但是并不会清掉缓存中第一个音频的数据,然后就又开始帮助手册了,看到了reset()函数。
reset()函数,可以清空播放器缓存中已有的音频数据,同时将播放器的缓存大小设置为0,播放器缓存大小为0就更不能播放出声音了。所以如果实现上述效果,需要在playData()函数中首先reset(),清空之前缓存的数据。重新setBufferSize(),重新start()使setBufferSize()有效,改动后代码如下:
bool PCMPlayer::initData(int nChannels,int SampleRate,int nSampleSize)
{
if(nullptr != m_audioOut){
return true;
}
QAudioFormat audioFmt;
audioFmt.setChannelCount(nChannels);
audioFmt.setCodec("audio/pcm");
audioFmt.setSampleRate(SampleRate);
audioFmt.setSampleSize(nSampleSize);
audioFmt.setByteOrder(QAudioFormat::LittleEndian);
audioFmt.setSampleType(QAudioFormat::SignedInt);
m_audioOut = new QAudioOutput(audioFmt);
/*m_audioOut->setBufferSize(10000000);*/
connect(m_audioOut,&QAudioOutput::stateChanged,this,[&](QAudio::State state){
if (QAudio::ActiveState == state)
{
m_bFinished = false;//新的一段开始播放,设置未播放完的标识位
}
else if(QAudio::IdleState == state)
{
if (!m_bFinished)//与QAudio::ActiveState中标识位的false对应
{
emit sig_PlayFinished();
}
}
});
/*m_device = m_audioOut->start();
if(nullptr == m_device){
return false;
}*/
return true;
}
void PCMPlayer::playData(QByteArray data)
{
m_bFinished = true;
if(nullptr != m_audioOut)
{
m_audioOut->reset();//清空缓存中原来的数据
if(!start())
{
return ;
}
}
if(nullptr != m_device){
m_device->write(data.toStdString().c_str(), data.length());
}
}
bool PCMPlayer::start()
{
if(nullptr == m_audioOut)
{
return false;
}
m_audioOut->setBufferSize(10000000);
m_device = m_audioOut->start();
if(nullptr == m_device){
return false;
}
return true;
}
至此,全部解决~