DirectSound 是微软 DirectX API 的一部分,主要用于在 Windows 平台上进行低级别的音频播放、混音和捕获,它提供了比标准 Windows API(如 PlaySound 或 waveOut* 函数)更强大的功能,
- 硬件加速:利用声卡的硬件混音器,可以同时播放多个声音而不会大量消耗 CPU。
- 精确控制:可以精确控制音量、声相(左右声道平衡)、频率等。
- 3D 音效:支持 3D 空间音效,让声音听起来有位置和距离感。
- 音频流:可以播放从文件或网络中实时读取的音频流。
核心概念
在开始编码之前,需要理解几个 DirectSound 的核心对象和概念:
IDirectSound:这是 DirectSound 的主接口对象,在初始化时创建,代表与音频设备的连接,所有其他操作都通过这个对象或其派生对象进行。IDirectSoundBuffer:这是主缓冲区,它代表声卡上的最终混音输出,你通常不需要直接操作它,但需要设置它的格式(如采样率、位深度、声道数),以确保所有次级缓冲区的声音都能被正确混音。IDirectSoundBuffer8:这是IDirectSoundBuffer的扩展接口,增加了更多功能(如设置音量、频率的SetVolume和SetFrequency方法),现代编程中,我们总是直接请求这个接口。IDirectSoundSecondaryBuffer/IDirectSoundSecondaryBuffer8:这是次级缓冲区,这是你实际用来加载和播放单个声音文件(如 WAV)的对象,你可以创建多个次级缓冲区,DirectSound 会在后台将它们混合到主缓冲区中,然后输出到扬声器,这是最常用的缓冲区类型。
编程步骤
使用 DirectSound 的典型流程如下:
- 初始化 COM 库:DirectSound 基于 COM (Component Object Model),所以第一步是调用
CoInitialize。 - 创建
IDirectSound对象:通过CoCreateInstance创建 DirectSound 对象。 - 设置协作级别:告诉 DirectSound 你的应用程序如何与其他应用程序共享音频设备。
DSSCL_PRIORITY是一个常用的级别,它允许你创建主缓冲区并设置其格式。 - 创建并设置主缓冲区:创建一个主缓冲区,并设置其音频格式(通常是 44.1kHz, 16-bit, Stereo)。
- 加载音频数据:从文件(通常是 WAV)中读取音频数据到内存。
- 创建次级缓冲区:使用加载的音频数据创建一个次级缓冲区。
- 播放声音:在次级缓冲区上调用
Play方法。 - 清理资源:使用完毕后,释放所有 COM 对件,并调用
CoUninitialize。
详细代码示例
下面是一个完整的 C 语言示例,演示如何播放一个简单的 WAV 文件,这个例子会创建一个控制台应用程序。
环境准备
你需要安装 Windows SDK,它包含了 DirectSound 的头文件 (dsound.h) 和库文件 (dsound.lib, dxguid.lib)。
在 Visual Studio 中,创建一个新的 "Windows Desktop Application" 项目会自动配置好这些链接,如果你使用 MinGW 或其他编译器,需要手动链接这些库。
代码
#define COBJMACROS // 允许直接使用 COM 对象的指针调用方法,如 pDS->Release()
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <mmsystem.h> // 用于 waveOut 函数和 WAVEFORMATEX
#include <dsound.h> // DirectSound 头文件
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "dsound.lib")
#pragma comment(lib, "dxguid.lib")
#pragma comment(lib, "winmm.lib")
// 全局变量
LPDIRECTSOUND8 g_pDS = NULL; // DirectSound 对象
LPDIRECTSOUNDBUFFER8 g_pDSBPrimary = NULL; // 主缓冲区
LPDIRECTSOUNDBUFFER8 g_pDSBSecondary = NULL; // 次级缓冲区(用于播放声音)
// 函数声明
HRESULT InitDirectSound(HWND hWnd);
HRESULT LoadAndPlayWAV(LPCSTR szFileName);
void Cleanup();
// 简单的 WAV 文件头结构 (仅用于演示,不完整)
typedef struct {
char RIFF[4];
DWORD dwSize;
char WAVE[4];
char fmt[4];
DWORD dwFmtSize;
WAVEFORMATEX wfex;
char data[4];
DWORD dwDataSize;
} WAVEHDR_EX;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
// 初始化 COM
if (FAILED(CoInitialize(NULL))) {
printf("CoInitialize failed!\n");
return 1;
}
// 获取窗口句柄 (控制台窗口)
HWND hWnd = GetConsoleWindow();
if (!hWnd) {
printf("GetConsoleWindow failed!\n");
CoUninitialize();
return 1;
}
// 初始化 DirectSound
if (FAILED(InitDirectSound(hWnd))) {
printf("InitDirectSound failed!\n");
CoUninitialize();
return 1;
}
// 加载并播放 WAV 文件
// 请确保你的项目目录下有一个名为 "test.wav" 的文件
// 或者修改 szFileName 为你的文件路径
if (FAILED(LoadAndPlayWAV("test.wav"))) {
printf("LoadAndPlayWAV failed!\n");
Cleanup();
CoUninitialize();
return 1;
}
printf("Sound playback started. Press any key to exit...\n");
getchar(); // 等待用户按键
Cleanup();
CoUninitialize();
return 0;
}
// 初始化 DirectSound
HRESULT InitDirectSound(HWND hWnd) {
HRESULT hr;
// 1. 创建 DirectSound 对象
hr = CoCreateInstance(&CLSID_DirectSound8, NULL, CLSCTX_ALL, &IID_IDirectSound8, (LPVOID*)&g_pDS);
if (FAILED(hr)) {
printf("CoCreateInstance for DirectSound8 failed (0x%x)\n", hr);
return hr;
}
// 2. 设置协作级别
hr = IDirectSound8_SetCooperativeLevel(g_pDS, hWnd, DSSCL_PRIORITY);
if (FAILED(hr)) {
printf("SetCooperativeLevel failed (0x%x)\n", hr);
return hr;
}
// 3. 创建主缓冲区
DSBUFFERDESC dsbd;
ZeroMemory(&dsbd, sizeof(DSBUFFERDESC));
dsbd.dwSize = sizeof(DSBUFFERDESC);
dsbd.dwFlags = DSBCAPS_PRIMARYBUFFER; // 标记为主缓冲区
dsbd.dwBufferBytes = 0; // 主缓冲区大小由驱动决定
dsbd.lpwfxFormat = NULL; // 主缓冲区格式单独设置
hr = IDirectSound8_CreateSoundBuffer(g_pDS, &dsbd, &g_pDSBPrimary, NULL);
if (FAILED(hr)) {
printf("CreateSoundBuffer for primary buffer failed (0x%x)\n", hr);
return hr;
}
// 4. 设置主缓冲区的格式
WAVEFORMATEX wfex;
wfex.wFormatTag = WAVE_FORMAT_PCM;
wfex.nChannels = 2; // 立体声
wfex.nSamplesPerSec = 44100; // 44.1 kHz
wfex.wBitsPerSample = 16; // 16-bit
wfex.nBlockAlign = (wfex.nChannels * wfex.wBitsPerSample) / 8;
wfex.nAvgBytesPerSec = wfex.nSamplesPerSec * wfex.nBlockAlign;
wfex.cbSize = 0;
hr = IDirectSoundBuffer8_SetFormat(g_pDSBPrimary, &wfex);
if (FAILED(hr)) {
printf("SetFormat for primary buffer failed (0x%x)\n", hr);
// 注意:即使失败,程序也可能继续运行,使用系统默认格式
}
printf("DirectSound initialized successfully.\n");
return S_OK;
}
// 加载 WAV 文件并创建次级缓冲区进行播放
HRESULT LoadAndPlayWAV(LPCSTR szFileName) {
HRESULT hr;
HANDLE hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open WAV file: %s\n", szFileName);
return E_FAIL;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
if (dwFileSize == 0) {
CloseHandle(hFile);
printf("WAV file is empty.\n");
return E_FAIL;
}
// 读取整个文件到内存
LPVOID pvFileData = LocalAlloc(LMEM_FIXED, dwFileSize);
if (!pvFileData) {
CloseHandle(hFile);
printf("Failed to allocate memory for WAV file.\n");
return E_FAIL;
}
DWORD dwRead;
if (!ReadFile(hFile, pvFileData, dwFileSize, &dwRead, NULL) || dwRead != dwFileSize) {
LocalFree(pvFileData);
CloseHandle(hFile);
printf("Failed to read WAV file.\n");
return E_FAIL;
}
CloseHandle(hFile);
WAVEHDR_EX* pWaveHdr = (WAVEHDR_EX*)pvFileData;
// 验证 RIFF 和 WAVE 标记
if (memcmp(pWaveHdr->RIFF, "RIFF", 4) != 0 || memcmp(pWaveHdr->WAVE, "WAVE", 4) != 0) {
LocalFree(pvFileData);
printf("Invalid WAV file format.\n");
return E_FAIL;
}
// 5. 创建次级缓冲区
DSBUFFERDESC dsbd;
ZeroMemory(&dsbd, sizeof(DSBUFFERDESC));
dsbd.dwSize = sizeof(DSBUFFERDESC);
dsbd.dwFlags = DSBCAPS_GLOBALFOCUS | DSBCAPS_GETCURRENTPOSITION2; // 常用标志
dsbd.dwBufferBytes = pWaveHdr->dwDataSize;
dsbd.lpwfxFormat = &pWaveHdr->wfex; // 使用 WAV 文件中的格式
hr = IDirectSound8_CreateSoundBuffer(g_pDS, &dsbd, &g_pDSBSecondary, NULL);
LocalFree(pvFileData); // 创建缓冲区后,可以立即释放文件数据
if (FAILED(hr)) {
printf("CreateSoundBuffer for secondary buffer failed (0x%x)\n", hr);
return hr;
}
// 6. 将音频数据写入次级缓冲区
LPVOID pLock1, pLock2;
DWORD dwLockSize1, dwLockSize2;
hr = IDirectSoundBuffer8_Lock(g_pDSBSecondary, 0, pWaveHdr->dwDataSize, &pLock1, &dwLockSize1, &pLock2, &dwLockSize2, 0);
if (SUCCEEDED(hr)) {
// 复制数据
memcpy(pLock1, (char*)pWaveHdr + sizeof(WAVEHDR_EX), dwLockSize1);
if (pLock2) {
memcpy(pLock2, (char*)pWaveHdr + sizeof(WAVEHDR_EX) + dwLockSize1, dwLockSize2);
}
IDirectSoundBuffer8_Unlock(g_pDSBSecondary, pLock1, dwLockSize1, pLock2, dwLockSize2);
}
// 7. 播放声音
hr = IDirectSoundBuffer8_Play(g_pDSBSecondary, 0, 0, 0); // 循环播放
if (FAILED(hr)) {
printf("Play failed (0x%x)\n", hr);
return hr;
}
printf("WAV file '%s' loaded and playing.\n", szFileName);
return S_OK;
}
// 释放所有资源
void Cleanup() {
if (g_pDSBSecondary) {
IDirectSoundBuffer8_Stop(g_pDSBSecondary);
IDirectSoundBuffer8_Release(g_pDSBSecondary);
g_pDSBSecondary = NULL;
}
if (g_pDSBPrimary) {
IDirectSoundBuffer8_Release(g_pDSBPrimary);
g_pDSBPrimary = NULL;
}
if (g_pDS) {
IDirectSound8_Release(g_pDS);
g_pDS = NULL;
}
printf("DirectSound resources cleaned up.\n");
}
如何编译和运行
-
准备一个 WAV 文件:将一个名为
test.wav的文件(使用 Windows 录音机创建的简单声音)放在你的项目.exe文件所在的目录下。 -
编译:
- Visual Studio: 创建项目,将上述代码粘贴到主
.c文件中,然后直接编译运行。 - 命令行 (使用 cl.exe):
cl /EHsc dsound_example.c /link dsound.lib dxguid.lib winmm.lib
- MinGW (gcc):
gcc -o dsound_example.exe dsound_example.c -ldsound -ldxguid -lwinmm
- Visual Studio: 创建项目,将上述代码粘贴到主
-
运行:编译成功后,运行生成的可执行文件,你应该会看到控制台输出,并且声音会开始播放,按任意键退出程序。
现代替代方案
虽然 DirectSound 非常经典,但对于新项目,微软推荐使用 XAudio2,XAudio2 是 DirectSound 的继任者,功能更强大、性能更高,并且设计更现代化。
XAudio2 的优势:
- 更清晰的架构:使用“源声部”(Source Voice)和“子声部”(Submix Voice)的概念,比 DirectSound 的缓冲区模型更灵活。
- 更好的性能:针对现代 CPU 和 GPU 架构进行了优化。
- 更强大的效果链:可以轻松地构建音频效果处理链(如 EQ、混响等)。
- 支持 XAPO:一个插件系统,可以自定义音频处理效果。
对于初学者来说,DirectSound 的概念相对简单,是学习 Windows 音频编程的一个很好的起点,但如果你打算开发新的商业游戏或应用程序,强烈建议直接学习 XAudio2。
