介紹 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。