DirectSound---簡易Wav播放器

這篇文章主要給你們介紹下如何用DirectSound打造一個簡易播放器,由於篇幅有限且代碼邏輯較爲複雜,咱們只介紹下核心技術內容。該播放器主要包括如下功能:git

  • 播放、暫停
  • 播放進度提示。

1. DirectSound播放概念簡介

1.1 播放相關概念

首先要介紹下DirectSound的設計理念:github

buffer-pic

在DirectSound中,你須要播放的音頻通常須要(也能夠直接放入主緩衝區,可是操做上比較困難並且對其餘DirectSound程序不太友好)放入一個被稱爲次緩衝區(Secondary Buffer)的地址區域中,該緩衝區由開發者人爲建立操控。因爲DirectSound支持多個音頻同時播放,因此咱們能夠建立多個緩衝區並同時播放。在播放時,放入次緩衝區的音頻先會被送入一個叫作主緩衝區(Primary Buffer)的地方進行混音,而後在送入硬件聲卡中進行播放。在Windows driver model,即WDM模式下,DirectSound實際上不能直接操做聲卡硬件,全部的混音操做不是送給主緩衝區而是被送往內核混音器(Kernel Mixer)進行混音,而後由內核混音器送往硬件。在WDM模式下,內核混音器替代了主緩衝區的功能位置。app

1.2 緩衝區相關概念

circle-buffer

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

2. 播放器實現

2.1 建立緩衝區

經過調用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!");
    }
}

2.2 預填充緩衝區

本人嘗試過直接在緩衝區頭部設置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;
}

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:

  1. 播放終止notify,則發出終止信號、退出線程。
  2. 數據填充notify,則填充數據、更新播放進度。
  3. 非終止非數據填充notify(發生在數據填充完成但播放未結束時),continue。

該線程一直等待這幾個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;
}

3. 運行結果

result1 result2 result3

完整代碼見連接

相關文章
相關標籤/搜索