音訊播放封裝(pcm格式,Windows平臺 c++)
阿新 • • 發佈:2018-12-17
介紹 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; }