DirectX/10.0/Direct3D/Direct Sound
本教程將介紹在 DirectX 11 中使用 Direct Sound 的基礎知識,以及如何載入和播放 .wav 音訊檔案。本教程基於之前 DirectX 11 教程中的程式碼。我將介紹 Direct Sound 在 DirectX 11 中的一些基礎知識,以及一些關於聲音格式的資訊,然後我們開始教程的程式碼部分。
您會注意到,在 DirectX 11 中,Direct Sound API 仍然與 DirectX 8 中的相同。唯一的重大區別是,最新的 Windows 作業系統通常不提供硬體聲音混合。原因是為了安全性和作業系統一致性,所有硬體呼叫現在都必須透過安全層。舊的音效卡曾經使用 DMA(直接記憶體訪問),它速度非常快,但在這種新的 Windows 安全模型下無法正常工作。因此,所有聲音混合現在都在軟體級別完成,因此該 API 不直接提供硬體加速。
Direct Sound 的好處是可以播放任何您想要的音訊格式。在本教程中,我介紹了 .wav 音訊格式,但您可以用 .mp3 或任何您喜歡的格式替換 .wav 程式碼。如果您建立了自己的音訊格式,甚至可以使用它。Direct Sound 非常易於使用,您只需建立具有所需回放格式的聲音緩衝區,然後將音訊格式複製到緩衝區的格式中,然後它就可以播放了。您可以理解為什麼這麼多應用程式使用 Direct Sound,因為它非常簡單。
請注意,Direct Sound 使用兩種不同的緩衝區,即主緩衝區和輔助緩衝區。主緩衝區是您預設音效卡、USB 耳機等上的主要聲音記憶體緩衝區。輔助緩衝區是您在記憶體中建立並載入聲音的緩衝區。當您播放輔助緩衝區時,Direct Sound API 會將該聲音混合到主緩衝區中,然後播放聲音。如果您同時播放多個輔助緩衝區,它會將它們混合在一起並在主緩衝區中播放它們。還要注意,所有緩衝區都是迴圈的,因此您可以將它們設定為無限期地重複。
為了開始本教程,我們將首先檢視更新後的框架。唯一的新類是 SoundClass,它包含所有 DirectSound 和 .wav 格式的功能。我已經刪除了其他類來簡化本教程。
SoundClass 封裝了 DirectSound 功能以及 .wav 音訊載入和播放功能。
/////////////////////////////////////////////////////////////////////////////// // Filename: soundclass.h /////////////////////////////////////////////////////////////////////////////// #ifndef _SOUNDCLASS_H_ #define _SOUNDCLASS_H_
以下庫和標頭檔案是 DirectSound 正確編譯所需的。
/////////////
// LINKING //
/////////////
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "winmm.lib")
//////////////
// INCLUDES //
//////////////
#include <windows.h>
#include <mmsystem.h>
#include <dsound.h>
#include <stdio.h>
///////////////////////////////////////////////////////////////////////////////
// Class name: SoundClass
///////////////////////////////////////////////////////////////////////////////
class SoundClass
{
private:
此處使用的 WaveHeaderType 結構用於 .wav 檔案格式。在載入 .wav 檔案時,我首先讀取標頭檔案以確定載入 .wav 音訊資料所需的必要資訊。如果您使用的是其他格式,則需要將此標頭檔案替換為您的音訊格式所需的標頭檔案。
struct WaveHeaderType
{
char chunkId[4];
unsigned long chunkSize;
char format[4];
char subChunkId[4];
unsigned long subChunkSize;
unsigned short audioFormat;
unsigned short numChannels;
unsigned long sampleRate;
unsigned long bytesPerSecond;
unsigned short blockAlign;
unsigned short bitsPerSample;
char dataChunkId[4];
unsigned long dataSize;
};
public:
SoundClass();
SoundClass(const SoundClass&);
~SoundClass();
Initialize 和 Shutdown 將處理本教程所需的一切。Initialize 函式將初始化 Direct Sound 並載入 .wav 音訊檔案,然後播放一次。Shutdown 將釋放 .wav 檔案並關閉 Direct Sound。
bool Initialize(HWND); void Shutdown(); private: bool InitializeDirectSound(HWND); void ShutdownDirectSound(); bool LoadWaveFile(char*, IDirectSoundBuffer8**); void ShutdownWaveFile(IDirectSoundBuffer8**); bool PlayWaveFile(); private: IDirectSound8* m_DirectSound; IDirectSoundBuffer* m_primaryBuffer;
請注意,我只包含一個輔助緩衝區,因為本教程只加載一個聲音。
IDirectSoundBuffer8* m_secondaryBuffer1; }; #endif
/////////////////////////////////////////////////////////////////////////////// // Filename: soundclass.cpp /////////////////////////////////////////////////////////////////////////////// #include "soundclass.h"
使用類建構函式初始化聲音類內部使用的私有成員變數。
SoundClass::SoundClass()
{
m_DirectSound = 0;
m_primaryBuffer = 0;
m_secondaryBuffer1 = 0;
}
SoundClass::SoundClass(const SoundClass& other)
{
}
SoundClass::~SoundClass()
{
}
bool SoundClass::Initialize(HWND hwnd)
{
bool result;
首先初始化 Direct Sound API 以及主緩衝區。初始化完成後,就可以呼叫 LoadWaveFile 函式,該函式將載入 .wav 音訊檔案並使用 .wav 檔案中的音訊資訊初始化輔助緩衝區。載入完成後,將呼叫 PlayWaveFile,然後播放 .wav 檔案一次。
// Initialize direct sound and the primary sound buffer.
result = InitializeDirectSound(hwnd);
if(!result)
{
return false;
}
// Load a wave audio file onto a secondary buffer.
result = LoadWaveFile("../Engine/data/sound01.wav", &m_secondaryBuffer1);
if(!result)
{
return false;
}
// Play the wave file now that it has been loaded.
result = PlayWaveFile();
if(!result)
{
return false;
}
return true;
}
Shutdown 函式首先使用 ShutdownWaveFile 函式釋放包含 .wav 檔案音訊資料的輔助緩衝區。完成此操作後,該函式將呼叫 ShutdownDirectSound,該函式將釋放主緩衝區和 DirectSound 介面。
void SoundClass::Shutdown()
{
// Release the secondary buffer.
ShutdownWaveFile(&m_secondaryBuffer1);
// Shutdown the Direct Sound API.
ShutdownDirectSound();
return;
}
InitializeDirectSound 處理獲取指向 DirectSound 和預設主聲音緩衝區的介面指標。請注意,您可以查詢系統以獲取所有聲音裝置,然後獲取指向特定裝置的主緩衝區的指標,但是為了簡化本教程,我只是獲取了指向預設聲音裝置的主緩衝區的指標。
bool SoundClass::InitializeDirectSound(HWND hwnd)
{
HRESULT result;
DSBUFFERDESC bufferDesc;
WAVEFORMATEX waveFormat;
// Initialize the direct sound interface pointer for the default sound device.
result = DirectSoundCreate8(NULL, &m_DirectSound, NULL);
if(FAILED(result))
{
return false;
}
// Set the cooperative level to priority so the format of the primary sound buffer can be modified.
result = m_DirectSound->SetCooperativeLevel(hwnd, DSSCL_PRIORITY);
if(FAILED(result))
{
return false;
}
我們必須設定訪問主緩衝區的描述。dwFlags 是此結構的重要部分。在本例中,我們只需要使用能夠調整其音量的主緩衝區描述。您可以獲取其他功能,但我們現在將其保持簡單。
// Setup the primary buffer description.
bufferDesc.dwSize = sizeof(DSBUFFERDESC);
bufferDesc.dwFlags = DSBCAPS_PRIMARYBUFFER | DSBCAPS_CTRLVOLUME;
bufferDesc.dwBufferBytes = 0;
bufferDesc.dwReserved = 0;
bufferDesc.lpwfxFormat = NULL;
bufferDesc.guid3DAlgorithm = GUID_NULL;
// Get control of the primary sound buffer on the default sound device.
result = m_DirectSound->CreateSoundBuffer(&bufferDesc, &m_primaryBuffer, NULL);
if(FAILED(result))
{
return false;
}
現在我們已經控制了預設聲音裝置上的主緩衝區,我們希望將其格式更改為我們想要的音訊檔案格式。在此,我決定我們要高品質的聲音,因此我們將將其設定為未壓縮的 CD 音訊質量。
// Setup the format of the primary sound buffer.
// In this case it is a .WAV file recorded at 44,100 samples per second in 16-bit stereo (cd audio format).
waveFormat.wFormatTag = WAVE_FORMAT_PCM;
waveFormat.nSamplesPerSec = 44100;
waveFormat.wBitsPerSample = 16;
waveFormat.nChannels = 2;
waveFormat.nBlockAlign = (waveFormat.wBitsPerSample / 8) * waveFormat.nChannels;
waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign;
waveFormat.cbSize = 0;
// Set the primary buffer to be the wave format specified.
result = m_primaryBuffer->SetFormat(&waveFormat);
if(FAILED(result))
{
return false;
}
return true;
}
ShutdownDirectSound 函式處理釋放主緩衝區和 DirectSound 介面。
void SoundClass::ShutdownDirectSound()
{
// Release the primary sound buffer pointer.
if(m_primaryBuffer)
{
m_primaryBuffer->Release();
m_primaryBuffer = 0;
}
// Release the direct sound interface pointer.
if(m_DirectSound)
{
m_DirectSound->Release();
m_DirectSound = 0;
}
return;
}
LoadWaveFile 函式負責載入 .wav 音訊檔案,然後將資料複製到新的輔助緩衝區。如果您要執行不同的格式,您需要替換此函式或編寫類似的函式。
bool SoundClass::LoadWaveFile(char* filename, IDirectSoundBuffer8** secondaryBuffer)
{
int error;
FILE* filePtr;
unsigned int count;
WaveHeaderType waveFileHeader;
WAVEFORMATEX waveFormat;
DSBUFFERDESC bufferDesc;
HRESULT result;
IDirectSoundBuffer* tempBuffer;
unsigned char* waveData;
unsigned char *bufferPtr;
unsigned long bufferSize;
首先開啟 .wav 檔案並讀取檔案頭。標頭檔案將包含有關音訊檔案的所有資訊,因此我們可以使用它來建立一個輔助緩衝區來容納音訊資料。音訊檔案頭還告訴我們資料從哪裡開始以及資料有多大。您會注意到我檢查了所有需要的標籤以確保音訊檔案沒有損壞並且是正確的波形檔案格式,其中包含 RIFF、WAVE、fmt、data 和 WAVE_FORMAT_PCM 標籤。我還進行了一些其他檢查以確保它是 44.1KHz 立體聲 16 位音訊檔案。如果它是單聲道、22.1 KHZ、8 位或其他任何東西,那麼它將失敗,從而確保我們只加載我們想要的精確格式。
// Open the wave file in binary.
error = fopen_s(&filePtr, filename, "rb");
if(error != 0)
{
return false;
}
// Read in the wave file header.
count = fread(&waveFileHeader, sizeof(waveFileHeader), 1, filePtr);
if(count != 1)
{
return false;
}
// Check that the chunk ID is the RIFF format.
if((waveFileHeader.chunkId[0] != 'R') || (waveFileHeader.chunkId[1] != 'I') ||
(waveFileHeader.chunkId[2] != 'F') || (waveFileHeader.chunkId[3] != 'F'))
{
return false;
}
// Check that the file format is the WAVE format.
if((waveFileHeader.format[0] != 'W') || (waveFileHeader.format[1] != 'A') ||
(waveFileHeader.format[2] != 'V') || (waveFileHeader.format[3] != 'E'))
{
return false;
}
// Check that the sub chunk ID is the fmt format.
if((waveFileHeader.subChunkId[0] != 'f') || (waveFileHeader.subChunkId[1] != 'm') ||
(waveFileHeader.subChunkId[2] != 't') || (waveFileHeader.subChunkId[3] != ' '))
{
return false;
}
// Check that the audio format is WAVE_FORMAT_PCM.
if(waveFileHeader.audioFormat != WAVE_FORMAT_PCM)
{
return false;
}
// Check that the wave file was recorded in stereo format.
if(waveFileHeader.numChannels != 2)
{
return false;
}
// Check that the wave file was recorded at a sample rate of 44.1 KHz.
if(waveFileHeader.sampleRate != 44100)
{
return false;
}
// Ensure that the wave file was recorded in 16 bit format.
if(waveFileHeader.bitsPerSample != 16)
{
return false;
}
// Check for the data chunk header.
if((waveFileHeader.dataChunkId[0] != 'd') || (waveFileHeader.dataChunkId[1] != 'a') ||
(waveFileHeader.dataChunkId[2] != 't') || (waveFileHeader.dataChunkId[3] != 'a'))
{
return false;
}
現在已經驗證了波形標頭檔案,我們可以設定我們將載入音訊資料的輔助緩衝區。我們必須首先設定輔助緩衝區的波形格式和緩衝區描述,與我們對主緩衝區所做的一樣。不過,在 dwFlags 和 dwBufferBytes 方面存在一些變化,因為這是輔助緩衝區而不是主緩衝區。
// Set the wave format of secondary buffer that this wave file will be loaded onto. waveFormat.wFormatTag = WAVE_FORMAT_PCM; waveFormat.nSamplesPerSec = 44100; waveFormat.wBitsPerSample = 16; waveFormat.nChannels = 2; waveFormat.nBlockAlign = (waveFormat.wBitsPerSample / 8) * waveFormat.nChannels; waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; waveFormat.cbSize = 0; // Set the buffer description of the secondary sound buffer that the wave file will be loaded onto. bufferDesc.dwSize = sizeof(DSBUFFERDESC); bufferDesc.dwFlags = DSBCAPS_CTRLVOLUME; bufferDesc.dwBufferBytes = waveFileHeader.dataSize; bufferDesc.dwReserved = 0; bufferDesc.lpwfxFormat = &waveFormat; bufferDesc.guid3DAlgorithm = GUID_NULL;
現在,建立輔助緩衝區的方法相當奇怪。第一步是使用您為輔助緩衝區設定的聲音緩衝區描述建立臨時 IDirectSoundBuffer。如果成功,則可以使用該臨時緩衝區透過呼叫 QueryInterface 並使用 IID_IDirectSoundBuffer8 引數來建立 IDirectSoundBuffer8 輔助緩衝區。如果成功,則可以釋放臨時緩衝區,輔助緩衝區就可以使用。
// Create a temporary sound buffer with the specific buffer settings.
result = m_DirectSound->CreateSoundBuffer(&bufferDesc, &tempBuffer, NULL);
if(FAILED(result))
{
return false;
}
// Test the buffer format against the direct sound 8 interface and create the secondary buffer.
result = tempBuffer->QueryInterface(IID_IDirectSoundBuffer8, (void**)&*secondaryBuffer);
if(FAILED(result))
{
return false;
}
// Release the temporary buffer.
tempBuffer->Release();
tempBuffer = 0;
現在輔助緩衝區已經準備就緒,我們可以載入音訊檔案中的波形資料。我首先將它載入到記憶體緩衝區中,這樣我就可以檢查和修改資料(如果需要)。資料在記憶體中後,鎖定輔助緩衝區,使用 memcpy 將資料複製到其中,然後解鎖它。此輔助緩衝區現在可以使用了。請注意,鎖定輔助緩衝區實際上可以接受兩個指標和兩個位置進行寫入。這是因為它是一個迴圈緩衝區,如果從中間開始寫入,則需要緩衝區從該點開始的大小,這樣就不會寫入緩衝區邊界之外。這對於流式音訊等很有用。在本教程中,我們建立了一個與音訊檔案大小相同的緩衝區,並從開頭開始寫入以簡化操作。
// Move to the beginning of the wave data which starts at the end of the data chunk header.
fseek(filePtr, sizeof(WaveHeaderType), SEEK_SET);
// Create a temporary buffer to hold the wave file data.
waveData = new unsigned char[waveFileHeader.dataSize];
if(!waveData)
{
return false;
}
// Read in the wave file data into the newly created buffer.
count = fread(waveData, 1, waveFileHeader.dataSize, filePtr);
if(count != waveFileHeader.dataSize)
{
return false;
}
// Close the file once done reading.
error = fclose(filePtr);
if(error != 0)
{
return false;
}
// Lock the secondary buffer to write wave data into it.
result = (*secondaryBuffer)->Lock(0, waveFileHeader.dataSize, (void**)&bufferPtr, (DWORD*)&bufferSize, NULL, 0, 0);
if(FAILED(result))
{
return false;
}
// Copy the wave data into the buffer.
memcpy(bufferPtr, waveData, waveFileHeader.dataSize);
// Unlock the secondary buffer after the data has been written to it.
result = (*secondaryBuffer)->Unlock((void*)bufferPtr, bufferSize, NULL, 0);
if(FAILED(result))
{
return false;
}
// Release the wave data since it was copied into the secondary buffer.
delete [] waveData;
waveData = 0;
return true;
}
ShutdownWaveFile 只釋放輔助緩衝區。
void SoundClass::ShutdownWaveFile(IDirectSoundBuffer8** secondaryBuffer)
{
// Release the secondary sound buffer.
if(*secondaryBuffer)
{
(*secondaryBuffer)->Release();
*secondaryBuffer = 0;
}
return;
}
PlayWaveFile 函式將播放儲存在輔助緩衝區中的音訊檔案。使用 Play 函式時,它會自動將音訊混合到主緩衝區中,如果還沒有播放,就會開始播放。還要注意,我們設定了位置以從輔助聲音緩衝區的開頭開始播放,否則它將從上次停止播放的位置繼續播放。由於我們設定了緩衝區的功能以允許我們控制聲音,因此此處我們將音量設定為最大。
bool SoundClass::PlayWaveFile()
{
HRESULT result;
// Set position at the beginning of the sound buffer.
result = m_secondaryBuffer1->SetCurrentPosition(0);
if(FAILED(result))
{
return false;
}
// Set volume of the buffer to 100%.
result = m_secondaryBuffer1->SetVolume(DSBVOLUME_MAX);
if(FAILED(result))
{
return false;
}
// Play the contents of the secondary sound buffer.
result = m_secondaryBuffer1->Play(0, 0, 0);
if(FAILED(result))
{
return false;
}
return true;
}
//////////////////////////////////////////////////////////////////////////////// // Filename: systemclass.h //////////////////////////////////////////////////////////////////////////////// #ifndef _SYSTEMCLASS_H_ #define _SYSTEMCLASS_H_ /////////////////////////////// // PRE-PROCESSING DIRECTIVES // /////////////////////////////// #define WIN32_LEAN_AND_MEAN ////////////// // INCLUDES // ////////////// #include <windows.h> /////////////////////// // MY CLASS INCLUDES // /////////////////////// #include "inputclass.h" #include "graphicsclass.h"
在此,我們包含新的 SoundClass 標頭檔案。
#include "soundclass.h"
////////////////////////////////////////////////////////////////////////////////
// Class name: SystemClass
////////////////////////////////////////////////////////////////////////////////
class SystemClass
{
public:
SystemClass();
SystemClass(const SystemClass&);
~SystemClass();
bool Initialize();
void Shutdown();
void Run();
LRESULT CALLBACK MessageHandler(HWND, UINT, WPARAM, LPARAM);
private:
void Frame();
void InitializeWindows(int&, int&);
void ShutdownWindows();
private:
LPCWSTR m_applicationName;
HINSTANCE m_hinstance;
HWND m_hwnd;
InputClass* m_Input;
GraphicsClass* m_Graphics;
我們為 SoundClass 物件建立一個新的私有變數。
SoundClass* m_Sound; }; ///////////////////////// // FUNCTION PROTOTYPES // ///////////////////////// static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); ///////////// // GLOBALS // ///////////// static SystemClass* ApplicationHandle = 0; #endif
我將只介紹自上一個教程以來更改過的函式。
////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "systemclass.h"
SystemClass::SystemClass()
{
m_Input = 0;
m_Graphics = 0;
在類建構函式中將新的 SoundClass 物件初始化為 null。
m_Sound = 0;
}
bool SystemClass::Initialize()
{
int screenWidth, screenHeight;
bool result;
// Initialize the width and height of the screen to zero before sending the variables into the function.
screenWidth = 0;
screenHeight = 0;
// Initialize the windows api.
InitializeWindows(screenWidth, screenHeight);
// Create the input object. This object will be used to handle reading the keyboard input from the user.
m_Input = new InputClass;
if(!m_Input)
{
return false;
}
// Initialize the input object.
result = m_Input->Initialize(m_hinstance, m_hwnd, screenWidth, screenHeight);
if(!result)
{
MessageBox(m_hwnd, L"Could not initialize the input object.", L"Error", MB_OK);
return false;
}
// Create the graphics object. This object will handle rendering all the graphics for this application.
m_Graphics = new GraphicsClass;
if(!m_Graphics)
{
return false;
}
// Initialize the graphics object.
result = m_Graphics->Initialize(screenWidth, screenHeight, m_hwnd);
if(!result)
{
return false;
}
在此,我們建立 SoundClass 物件,然後將其初始化以供使用。請注意,在本教程中,初始化也將啟動波形檔案的播放。
// Create the sound object.
m_Sound = new SoundClass;
if(!m_Sound)
{
return false;
}
// Initialize the sound object.
result = m_Sound->Initialize(m_hwnd);
if(!result)
{
MessageBox(m_hwnd, L"Could not initialize Direct Sound.", L"Error", MB_OK);
return false;
}
return true;
}
void SystemClass::Shutdown()
{
在 SystemClass::Shutdown 中,我們還關閉 SoundClass 物件並釋放它。
// Release the sound object.
if(m_Sound)
{
m_Sound->Shutdown();
delete m_Sound;
m_Sound = 0;
}
// Release the graphics object.
if(m_Graphics)
{
m_Graphics->Shutdown();
delete m_Graphics;
m_Graphics = 0;
}
// Release the input object.
if(m_Input)
{
m_Input->Shutdown();
delete m_Input;
m_Input = 0;
}
// Shutdown the window.
ShutdownWindows();
return;
}
該引擎現在支援 Direct Sound 的基礎知識。它目前只在您啟動程式時播放一個波形檔案一次。
1. 重新編譯程式並確保它以立體聲播放波形檔案。結束後按Esc鍵關閉視窗。
2. 用你自己的44.1KHz 16bit 2通道音訊波形檔案替換sound01.wav檔案,然後再次執行程式。
3. 重寫程式以載入兩個波形檔案並同時播放它們。
4. 將波形更改為迴圈播放,而不是隻播放一次。