DirectSound---簡易Wav播放器

来源:https://www.cnblogs.com/lgxZJ/archive/2018/02/15/8449442.html
-Advertisement-
Play Games

這篇文章主要給大家介紹下如何用DirectSound打造一個簡易播放器,因為篇幅有限且代碼邏輯較為複雜,我們只介紹下核心技術內容。該播放器主要包括以下功能: 播放、暫停 播放進度提示。 1. DirectSound播放概念簡介 1.1 播放相關概念 首先要介紹下DirectSound的設計理念: ! ...


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

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

1. DirectSound播放概念簡介

1.1 播放相關概念

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

buffer-pic

在DirectSound中,你需要播放的音頻一般需要(也可以直接放入主緩衝區,但是操作上比較困難而且對其他DirectSound程式不太友好)放入一個被稱為次緩衝區(Secondary Buffer)的地址區域中,該緩衝區由開發者人為創建操控。由於DirectSound支持多個音頻同時播放,所以我們可以創建多個緩衝區並同時播放。在播放時,放入次緩衝區的音頻先會被送入一個叫做主緩衝區(Primary Buffer)的地方進行混音,然後在送入硬體音效卡中進行播放。在Windows driver model,即WDM模式下,DirectSound實際上不能直接操作音效卡硬體,所有的混音操作不是送給主緩衝區而是被送往內核混音器(Kernel Mixer)進行混音,然後由內核混音器送往硬體。在WDM模式下,內核混音器替代了主緩衝區的功能位置。

1.2 緩衝區相關概念

circle-buffer

DirectSound的緩衝區類別大體可以分為兩種:1) 靜態緩衝區,2) 流緩衝區。靜態緩衝區就是一段較短的音頻全部填充到一個緩衝區中,然後從頭到尾播放;流緩衝區可以描述為音頻流,實際上這種流也是通過單個有長度的緩衝區來抽象模擬的。在流緩衝區模式下,單個緩衝區會被重覆填充和播放,也就是說當DirectSound播放到緩衝區的最後一個尾部時,它會回到緩衝區的頭部繼續開始播放。因此,在播放較長的音頻文件時需要開發者手動迴圈填充緩衝區。

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,並對不同情況進行不同的處理:

  1. 播放終止notify,則發出終止信號、退出線程。
  2. 數據填充notify,則填充數據、更新播放進度。
  3. 非終止非數據填充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. 運行結果

result1 result2 result3

完整代碼見鏈接


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、前言 接著上一章的內容,繼續本章的學習。 二、內容 ...
  • 我也在FreeCodeCamp上碰到這樣一道題: 刪除數組中的所有假值。 在JavaScript中,假值有false、null、0、”“、undefined 和 NaN。 對於NaN的判斷,JS提供了函數isNaN()。但是使用isNaN()函數只能判斷變數是否非數字,而無法判斷變數值是否為NaN。 ...
  • 一、前言 接著上一章的內容,繼續JQuery的學習 二、內容 ...
  • // A input { outline: none; } // B Input::-moz-focus-inner { border: none; } //加了ouline沒用,別死磕,重啟 ...
  • 使用分散式系統與在單機系統中處理問題有很大的區別,分散式系統帶來了更大的處理能力和存儲容量之後,也帶來了很多新的 "煩惱" 。在這一篇之中,我們將看看分散式系統帶給我們新的挑戰。 1.故障 當我們在使用單機系統時,它通常以一種相當可預測的方式工作:要麼它正常工作,要麼不工作。 而當我們在使用分散式系 ...
  • 源代碼是這樣:s=b'^SdVkT#S ]`Y\\!^)\x8f\x80ism'key=''for i in s: i=ord(i)-16 key+=chr(i^32)print (key)運行後出現了問題:ord() expected string of length 1, but int fou... ...
  • 本章主要內容: 1)函數重載 2)C++調用C代碼 3)new/delete關鍵字實現動態記憶體分配 4)namespace命名空間 大家都知道,在生活中,動詞和不同的名詞搭配一起,意義都會大有不同,比如”玩”: 玩游戲 玩卡牌 玩足球 所以在C++中,便出現了函數重載(JAVA,c#等語言都有函數重 ...
  • 一、問題 今天筆者在構建maven相關的web項目的時候,出現了一個問題: Java compiler level does not match the version of the installed Java project facet. 二、分析 導致這個問題的原因是因為jdk版本的問題,但是 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...