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. 将波形更改为循环播放,而不是只播放一次。