音频播放封装(pcm格式,Windows平台 c++)
介绍 pcm格式是音频非压缩格式。如果要对音频文件播放,需要先转换为pcm格式。
windows提供了多套函数用于播放,本文介绍Waveform Audio Functions系列函数。
原始的播放函数比较难用,因工作需要,我写了一个播放器,将播放相关函数封装了;非常好用,还不易出错。
播放流程
程序头文件 可以根据头文件窥探函数功能,下面再做简单介绍。
class CPcmPlay { public: CPcmPlay(); ~CPcmPlay(); //是否打开了 播放设备 BOOL IsOpen(); //nSamplesPerSec 采样频率 8000 //采样位数 :8,16 //声道个数: 1 BOOL Open(int nSamplesPerSec, int wBitsPerSample, int nChannels); //设置声音大小 0到100 BOOL SetVolume(int volume); //播放内存数据 //异步播放,block指针数据可以立即删除 MMRESULT Play(LPSTR block, DWORD size); void StopPlay(); //停止播放 BOOL IsOnPlay(); //是否有数据在播放 void Close();//关闭播放设备 double GetCurPlaySpan(); //获取当前块已播放的时长 double GetLeftPlaySpan(); //获取剩余播放播放的时长 BOOL IsNoPlayBuffer();//打开音频还没播放过 private: void OnOpen(); void OnClose(); void OnDone(WAVEHDR *header); void AddHeader(WAVEHDR *header); void DelHeader(WAVEHDR *header); //根据数据长度,计算播放长度 单位秒 double GetPlayTimeSpan(int bufferLen); void static CALLBACK MyWaveOutProc(HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2); private: UINT64 m_totalPlayBuffer; WAVEFORMATEX m_waveForm; HWAVEOUT m_hWaveOut; std::list<WAVEHDR*> m_listWaveOutHead; CCritical m_listLock; };
1)打开音频设备
BOOL CPcmPlay::Open(int nSamplesPerSec,int wBitsPerSample,int nChannels) { if (IsOpen()) return FALSE; { CCriticalLock lock(m_listLock); m_listWaveOutHead.clear(); } m_totalPlayBuffer = 0; m_waveForm.nSamplesPerSec = nSamplesPerSec; /* sample rate */ m_waveForm.wBitsPerSample = wBitsPerSample; /* sample size */ m_waveForm.nChannels = nChannels; /* channels*/ m_waveForm.cbSize = 0; /* size of _extra_ info */ m_waveForm.wFormatTag = WAVE_FORMAT_PCM; m_waveForm.nBlockAlign = (m_waveForm.wBitsPerSample * m_waveForm.nChannels) >> 3; m_waveForm.nAvgBytesPerSec = m_waveForm.nBlockAlign * m_waveForm.nSamplesPerSec; if (waveOutOpen(&m_hWaveOut, WAVE_MAPPER, &m_waveForm, (DWORD_PTR)MyWaveOutProc, (DWORD_PTR)this, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) { return FALSE; } return TRUE; }
需要先设置pcm格式,pcm相关介绍请参考别的文章。
打开音频传入的有个参数值为CALLBACK_FUNCTION,表示播放事件,通过函数回调方式通知。
由于音频播放是异步的,当音频播放完毕、音频设备关闭等消息,需要一个通知机制。回调函数如下:
void CALLBACK CPcmPlay::MyWaveOutProc( HWAVEOUT hwo, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2 ) { CPcmPlay *play = (CPcmPlay*)dwInstance; if (uMsg == WOM_OPEN) //音频打开 { play->OnOpen(); return; } if (uMsg == WOM_CLOSE) //音频句柄关闭 { play->OnClose(); return; } if (uMsg == WOM_DONE)//音频缓冲播放完毕 { WAVEHDR *header = (WAVEHDR*)dwParam1; play->OnDone(header); } }
waveOutOpen 传入参数与回调函数的参数有一定关联。waveOutOpen传入参数(DWORD_PTR)this,就是回调函数的DWORD_PTR dwInstance;通过这种关联,就可以找到类变量(CPcmPlay *play = (CPcmPlay*)dwInstance;)。
2)播放数据
MMRESULT CPcmPlay::Play(LPSTR block, DWORD size) { if (m_hWaveOut == NULL) return MMSYSERR_INVALHANDLE; WAVEHDR *header = new WAVEHDR(); ZeroMemory(header, sizeof(WAVEHDR)); //对应回调函数 DWORD_PTR dwParam1, header->dwUser = (DWORD_PTR)header; //new新的数据,并将block数据复制。 //这样函数返回,block的数据可以立即释放 LPSTR blockNew = new char[size]; memcpy(blockNew, block, size); header->dwBufferLength = size; header->lpData = blockNew; //准备数据 MMRESULT result = waveOutPrepareHeader(m_hWaveOut, header, sizeof(WAVEHDR)); if (result != MMSYSERR_NOERROR) { FreeWaveHeader(header); return result; } //播放数据加入缓冲队列 //播放时异步的,播放完毕之前,缓冲的数据不能释放 AddHeader(header); result = waveOutWrite(m_hWaveOut, header, sizeof(WAVEHDR)); if (result != MMSYSERR_NOERROR) { DelHeader(header); return result; } m_totalPlayBuffer += size; return MMSYSERR_NOERROR; }
有一点特别注意,播放函数是异步的,就是播放完毕之前,播放缓冲数据不能释放。为了方便调用,重新将输入参数block的数据又new一块内存存放,调用方不必关心内存块啥时释放。
我们将播放缓冲加入一个list列表中,当播放完毕,我们需要释放该缓冲。怎么知道缓冲数据是否播放完毕?是通过回调机制。参加前文回调函数。
if (uMsg == WOM_DONE)//音频缓冲播放完毕 { //对应回调函数 DWORD_PTR dwParam1, //header->dwUser = (DWORD_PTR)header; WAVEHDR *header = (WAVEHDR*)dwParam1; play->OnDone(header); }
回调参数dwParam1对应header->dwUser,我们将dwUser设置为缓冲指针,这样,通过回调函数的参数就找到了对应播放缓冲。
播放完毕的缓冲,需要释放。
void CPcmPlay::DelHeader(WAVEHDR *header) { { CCriticalLock lock(m_listLock); m_listWaveOutHead.remove(header); } FreeWaveHeader(header); } void FreeWaveHeader(WAVEHDR *header) { delete[]header->lpData; delete header; }
由于回调函数和播放函数属于不同的线程,所以对列表操作加了锁。
3 关闭音频播放
void CPcmPlay::Close() { if (m_hWaveOut == NULL) return; StopPlay(); MMRESULT result = waveOutClose(m_hWaveOut); m_hWaveOut = NULL; //等待释放所有的播放缓冲 int n = 0; while (IsOnPlay() && n < 5000) { n++; ::Sleep(1); } }
关闭播放时,有一点需要注意,有可能播放还没完毕。调用waveOutClose后,回调函数给通知,即uMsg == WOM_DONE,在回调函数中将缓冲数据释放。
当所有的数据释放完毕,才能安全退出。
这就是播放的基本流程,其实不难。但是,因为播放是异步的,所以处理缓冲释放方面有点小技巧。
当然本类对其他一些函数也做了封装,方便调用,代码如下:
//根据数据长度,计算播放长度 单位秒 double CPcmPlay::GetPlayTimeSpan(int bufferLen) { if (m_waveForm.nSamplesPerSec == 0 || m_waveForm.nSamplesPerSec == 0) return 0; double n = m_waveForm.nSamplesPerSec*m_waveForm.wBitsPerSample /8; double result = ((double)bufferLen)/n; return result; } //设置音量大小 volume取值范围0--100 BOOL CPcmPlay::SetVolume(int volume) { if (m_hWaveOut == NULL) return FALSE; UINT16 n = volume; if (volume <= 0) n = 0; if (volume >= 100) n = 100; n = n * 0xFFFF / 100; DWORD dwVolume = n; dwVolume = (dwVolume << 16); dwVolume += n; MMRESULT result = waveOutSetVolume(m_hWaveOut, dwVolume); return (result == MMSYSERR_NOERROR); } //获取已播放时长 单位秒 double CPcmPlay::GetCurPlaySpan() { if (m_hWaveOut == NULL) return 0; MMTIME mm = { 0 }; mm.wType = TIME_BYTES; MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm)); if (mm.wType != TIME_BYTES || result != MMSYSERR_NOERROR) return 0; double span = GetPlayTimeSpan(mm.u.cb); return span; } //获取剩余播放时长 单位秒 double CPcmPlay::GetLeftPlaySpan() { if (m_hWaveOut == NULL) return 0; MMTIME mm = { 0 }; mm.wType = TIME_BYTES; MMRESULT result = waveOutGetPosition(m_hWaveOut, &mm, sizeof(mm)); if (mm.wType != TIME_BYTES || result != MMSYSERR_NOERROR) return 0; double span = GetPlayTimeSpan(m_totalPlayBuffer - mm.u.cb); return span; }
封装类下载地址https://download.csdn.net/download/qq_29939347/10746435。
专注C#、C++。擅长WPF、WinForm、QT等技术。
研究ofd多年,开发了一些列产品。
技术交流QQ群:565438497。