這篇文章主要給你們介紹下如何用DirectSound打造一個簡易播放器,由於篇幅有限且代碼邏輯較爲複雜,咱們只介紹下核心技術內容。該播放器主要包括如下功能:git
首先要介紹下DirectSound的設計理念:github
在DirectSound中,你須要播放的音頻通常須要(也能夠直接放入主緩衝區,可是操做上比較困難並且對其餘DirectSound程序不太友好)放入一個被稱爲次緩衝區(Secondary Buffer)的地址區域中,該緩衝區由開發者人爲建立操控。因爲DirectSound支持多個音頻同時播放,因此咱們能夠建立多個緩衝區並同時播放。在播放時,放入次緩衝區的音頻先會被送入一個叫作主緩衝區(Primary Buffer)的地方進行混音,而後在送入硬件聲卡中進行播放。在Windows driver model,即WDM模式下,DirectSound實際上不能直接操做聲卡硬件,全部的混音操做不是送給主緩衝區而是被送往內核混音器(Kernel Mixer)進行混音,而後由內核混音器送往硬件。在WDM模式下,內核混音器替代了主緩衝區的功能位置。app
DirectSound的緩衝區類別大致能夠分爲兩種:1) 靜態緩衝區,2) 流緩衝區。靜態緩衝區就是一段較短的音頻所有填充到一個緩衝區中,而後從頭至尾播放;流緩衝區能夠描述爲音頻流,實際上這種流也是經過單個有長度的緩衝區來抽象模擬的。在流緩衝區模式下,單個緩衝區會被重複填充和播放,也就是說當DirectSound播放到緩衝區的最後一個尾部時,它會回到緩衝區的頭部繼續開始播放。所以,在播放較長的音頻文件時須要開發者手動循環填充緩衝區。ide
DirectSound中還有遊標(cursor)的概念,遊標分兩種:1) 播放遊標(play cusror),2) 寫入遊標(write cursor)。顧名思義,播放遊標指向當前播放的地址,寫入遊標指向當前能夠寫入的開始地址,寫入遊標老是在播放遊標前面,且二者之間的數據塊已經被DirectSound預約,不能被寫入。其中,播放指針能夠經過函數來更改,而寫入指針由DirectSound本身控制,開發者不能操做它。一旦次緩衝區設定好音頻格式,在播放中這兩個遊標會一直保持固定的間距:若是沒記錯,採樣率44100Hz、2聲道、8比特的音頻數據,二者的位置間隔660字節,也就是1/70
秒的數據。函數
爲了在適當的時候填充下一塊要播放的數據,DirectSound提供了notify的功能:當播放到某一個緩衝區位置的時候,他會提醒你。該notify功能的實現經過Windows的事件對象(Event Object)實現,也就是說你須要等待這個事件被喚醒,在GUI程序中,這一般意味着你須要另起一個線程。oop
經過調用IDirectSound8::CreateSoundBuffer(...)
函數,咱們建立一個可以容納seconds秒的次緩衝區。參數DSBUFFERDESC中須要指定DSBCAPS_CTRLPOSITIONNOTIFY、DSBCAPS_GETCURRENTPOSITION2
,前者容許咱們設置notify,後者保證咱們在調用IDirectSoundBuffer8::GetCurrentPosition(...)
時播放遊標的位置比較準確。ui
void WavPlayer::createBufferOfSeconds(unsigned seconds) { DSBUFFERDESC bufferDescription; bufferDescription.dwSize = sizeof(bufferDescription); bufferDescription.dwFlags = DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2 | DSBCAPS_LOCDEFER ; bufferDescription.dwBufferBytes = m_secondaryBufferSize = m_wavFile.getWaveFormat().nAvgBytesPerSec * seconds; bufferDescription.dwReserved = 0; bufferDescription.lpwfxFormat = &m_wavFile.getWaveFormat(); bufferDescription.guid3DAlgorithm = GUID_NULL; IDirectSoundBuffer* soundBuffer; if (m_directSound8->CreateSoundBuffer(&bufferDescription, &soundBuffer, NULL) != DS_OK) { throw std::exception("create secondary buffer failed:CreateSoundBuffer"); } if (soundBuffer->QueryInterface(IID_IDirectSoundBuffer8, (LPVOID*)&m_soundBufferInterface) != S_OK) { throw std::exception("IDirectSoundBuffer8 interface not supported!"); } }
本人嘗試過直接在緩衝區頭部設置notify,使數據的填充比較天然。大多數狀況下這樣沒有問題,可是在電腦cpu負載較高時會形成音頻毛刺,效果不盡如人意。所以我選擇預填充數據,防止這類狀況出現。this
void WavPlayer::fillDataIntoBuffer() { Q_ASSERT(m_bufferSliceCount > 1); // fill half buffer to signal the notify event to do next data filling LPVOID firstAudioAddress; LPVOID secondAudioAddress; DWORD firstAudioBytes; DWORD secondAudioBytes; HRESULT result = m_soundBufferInterface->Lock(0, m_secondaryBufferSize / m_bufferSliceCount, &firstAudioAddress, &firstAudioBytes, &secondAudioAddress, &secondAudioBytes, 0); if (result == DSERR_BUFFERLOST) { result = m_soundBufferInterface->Restore(); } if (result != DS_OK) { throw std::exception("Cannot lock entire secondary buffer(restore tryed)"); } Q_ASSERT(firstAudioBytes == m_secondaryBufferSize / m_bufferSliceCount && secondAudioAddress == nullptr && secondAudioBytes == 0); m_nextDataToPlay = static_cast<char*>(m_wavFile.getAudioData()); CopyMemory(firstAudioAddress, m_nextDataToPlay, firstAudioBytes); if (m_soundBufferInterface->Unlock(firstAudioAddress, firstAudioBytes, secondAudioAddress, secondAudioBytes) != DS_OK) { throw std::exception("Unlick failed when fill data into secondary buffer"); } m_nextDataToPlay += firstAudioBytes; }
爲了在運行時循環填充數據,咱們先要設置notify,這裏的notify比較複雜,包含了3種類別:線程
其中,第二種notify可能會也可能不會與第一種notify重合,在不重合狀況下咱們才新分配一個notify:設計
m_additionalNotifyIndex = 0; if (m_additionalEndNotify) for (unsigned i = 1; i < m_bufferSliceCount; ++i) if (bufferEndOffset < (m_secondaryBufferSize / m_bufferSliceCount * i)) { m_additionalNotifyIndex = i; break; } // add a stop notify count at the end of entire notifies to make the data filling // thread exit gracefully ++m_notifyCount; m_notifyHandles = static_cast<HANDLE*>(malloc(sizeof(HANDLE)* (m_notifyCount))); if (m_notifyHandles == nullptr) throw std::exception("malloc error"); m_notifyOffsets = static_cast<DWORD*>(malloc(sizeof(DWORD)* (m_notifyCount))); if (m_notifyHandles == nullptr) throw std::exception("malloc error"); for (unsigned i = 0; i < m_notifyCount; ++i) { m_notifyHandles[i] = CreateEvent(NULL, FALSE, FALSE, NULL); if (m_notifyHandles[i] == NULL) throw std::exception("CreateEvent error"); if (m_additionalEndNotify && i == m_additionalNotifyIndex) { // set buffer end notify m_notifyOffsets[i] = bufferEndOffset; m_endNotifyHandle = m_notifyHandles[i]; } else if (i == m_notifyCount - 1) { // do nothing } else { // NOTE: the entire buffer size must can be devided by this `notifyCount`, // or it will lost some bytes when filling data into the buffer. since the end // notify is inside the notify count, we need to calculate the buffer slice index. unsigned bufferSliceIndex = getBufferIndexFromNotifyIndex(i); m_notifyOffsets[i] = m_secondaryBufferSize / m_bufferSliceCount * bufferSliceIndex; if (!m_additionalEndNotify && m_notifyOffsets[i] == bufferEndOffset) m_endNotifyHandle = m_notifyHandles[i]; } } // skip the exit notify which we toggle explicitly setNotifyEvent(m_notifyHandles, m_notifyOffsets, m_notifyCount - 1);
該線程內包含多種類別的notify:
該線程一直等待這幾個notify,並對不一樣狀況進行不一樣的處理:
DWORD WINAPI WavPlayer::dataFillingThread(LPVOID param) { WavPlayer* wavPlayer = reinterpret_cast<WavPlayer*>(param); while (!wavPlayer->m_quitDataFillingThread) { try { DWORD notifyIndex = WaitForMultipleObjects(wavPlayer->m_notifyCount, wavPlayer->m_notifyHandles, FALSE, INFINITE); if (!(notifyIndex >= WAIT_OBJECT_0 && notifyIndex <= WAIT_OBJECT_0 + wavPlayer->m_notifyCount - 1)) throw std::exception("WaitForSingleObject error"); if (notifyIndex == wavPlayer->m_notifyCount - 1) break; // each notify represents one second(or approximately one second) except the exit notify if (!(wavPlayer->m_additionalNotifyIndex == notifyIndex && wavPlayer->m_endNotifyLoopCount > 0)) { ++wavPlayer->m_currentPlayingTime; wavPlayer->sendProgressUpdatedSignal(); } // if return false, the audio ends if (tryToFillNextBuffer(wavPlayer, notifyIndex) == false) { wavPlayer->stop(); ++wavPlayer->m_currentPlayingTime; wavPlayer->sendProgressUpdatedSignal(); wavPlayer->sendAudioEndsSignal(); // not break the loop, we need to update the audio progress although data filling ends } } catch (std::exception& exception) { OutputDebugStringA("exception in data filling thread:"); OutputDebugStringA(exception.what()); } } return 0; }
完整代碼見連接。