DirectSound---簡易Wav播放器
這篇文章主要給大家介紹下如何用DirectSound打造一個簡易播放器,因為篇幅有限且代碼邏輯較為復雜,我們只介紹下核心技術內容。該播放器主要包括以下功能:
- 播放、暫停
- 播放進度提示。
1. DirectSound播放概念簡介
1.1 播放相關概念
首先要介紹下DirectSound的設計理念:
在DirectSound中,你需要播放的音頻一般需要(也可以直接放入主緩沖區,但是操作上比較困難而且對其他DirectSound程序不太友好)放入一個被稱為次緩沖區(Secondary Buffer)的地址區域中,該緩沖區由開發者人為創建操控。由於DirectSound支持多個音頻同時播放,所以我們可以創建多個緩沖區並同時播放。在播放時,放入次緩沖區的音頻先會被送入一個叫做主緩沖區
1.2 緩沖區相關概念
DirectSound的緩沖區類別大體可以分為兩種:1) 靜態緩沖區,2) 流緩沖區。靜態緩沖區就是一段較短的音頻全部填充到一個緩沖區中,然後從頭到尾播放;流緩沖區可以描述為音頻流,實際上這種流也是通過單個有長度的緩沖區來抽象模擬的。在流緩沖區模式下,單個緩沖區會被重復填充和播放
DirectSound中還有遊標(cursor)的概念,遊標分兩種:1) 播放遊標(play cusror),2) 寫入遊標(write cursor)。顧名思義,播放遊標指向當前播放的地址,寫入遊標指向當前可以寫入的開始地址,寫入遊標總是在播放遊標前面,且兩者之間的數據塊已經被DirectSound預定,不能被寫入。其中,播放指針可以通過函數來更改,而寫入指針由DirectSound自己控制,開發者不能操作它。一旦次緩沖區設定好音頻格式,在播放中這兩個遊標會一直保持固定的間距:如果沒記錯,采樣率44100Hz、2聲道、8比特的音頻數據,兩者的位置間隔660字節,也就是1/70
為了在適當的時候填充下一塊要播放的數據,DirectSound提供了notify的功能:當播放到某一個緩沖區位置的時候,他會提醒你。該notify功能的實現通過Windows的事件對象(Event Object)實現,也就是說你需要等待這個事件被喚醒,在GUI程序中,這通常意味著你需要另起一個線程。
2. 播放器實現
2.1 創建緩沖區
通過調用IDirectSound8::CreateSoundBuffer(...)
函數,我們創建一個能夠容納seconds秒的次緩沖區。參數DSBUFFERDESC中需要指定DSBCAPS_CTRLPOSITIONNOTIFY、DSBCAPS_GETCURRENTPOSITION2
,前者允許我們設置notify,後者保證我們在調用IDirectSoundBuffer8::GetCurrentPosition(...)
時播放遊標的位置比較準確。
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!");
}
}
2.2 預填充緩沖區
本人嘗試過直接在緩沖區頭部設置notify,使數據的填充比較自然。大多數情況下這樣沒有問題,但是在電腦cpu負載較高時會造成音頻毛刺,效果不盡如人意。因此我選擇預填充數據,防止這類情況出現。
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;
}
2.3 設置緩沖區notify
為了在運行時循環填充數據,我們先要設置notify,這裏的notify比較復雜,包含了3種類別:
- 數據填充notify。
- 音頻播放終止notify。
- 退出notify。(為了優雅的退出填充線程,我們選擇在退出播放時喚醒線程)
其中,第二種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);
2.4 創建數據填充線程、播放進度更新
該線程一直等待多個notify,並對不同情況進行不同的處理:
- 播放終止notify,則發出終止信號、退出線程。
- 數據填充notify,則填充數據、更新播放進度。
- 非終止非數據填充notify(發生在數據填充完成但播放未結束時),continue。
DWORD WINAPI WavPlayer::dataFillingThread(LPVOID param)
{
WavPlayer* wavPlayer = reinterpret_cast
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;
}
3. 運行結果
完整代碼見鏈接。
DirectSound---簡易Wav播放器