资讯动态

DirectSound音频播放器VC++代码解析:从初始化到流式播放

发布时间:2026/10/15 12:13:03 来源:尧图企业网站定制
简介这份资源是一套基于DirectSound的VC音乐播放器完整工程源码面向具备一定C基础、希望入门Windows音频编程的开发者。它围绕DirectSound8的2D音频处理能力演示了从初始化IDirectSound8接口、设置协作级别到创建主次缓冲区、加载音乐文件、播放与音量控制、错误处理及资源释放的完整链路可作为理解低延迟音频输出的实操起点。压缩包共23个文件约1.07MB以5个h头文件与4个cpp源文件为核心辅以2个wav音频、ico图标、rc资源脚本及dsp、dsw等VC工程文件另含exe可执行程序便于直接编译运行与对照调试。目前已有257人学习下载。读者可从中获得一套可直接编译的播放器工程、缓冲区管理与播放进度监控的代码范例以及音量调节、停止播放和HRESULT错误排查的参考实现适合作为音频编程练手或二次开发的基础模板。1. 从一份老旧的 VC 音频工程说起为什么还要看 DirectSound如果你手头有一份基于 DirectSound 的音乐播放器 VC 代码第一反应可能是“这玩意儿不是被 WASAPI 和 XAudio2 取代了吗”。确实微软从 Windows Vista 开始就把 DirectSound 放进了兼容层底层最终走的是 WASAPI 会话。但现实是大量工业控制上位机、老旧医疗设备界面、教学演示程序、甚至某些游戏外设的配套工具仍然跑在 DirectSound 上。原因很直接它的 API 足够薄初始化路径短延迟在多数场景下够用而且 VC 工程一旦编译通过十年后换台机器照样能跑。这份代码资源的核心价值不在于“新”而在于“完整可编译”。它把音频设备枚举、次级缓冲区管理、WAV 文件解析、播放控制、循环与停止逻辑全部串起来了。适合两类人一是正在维护老项目的工程师需要快速定位播放卡顿或设备切换失败的问题二是想理解音频播放底层链路的学习者DirectSound 的模型比 WASAPI 简单得多适合拿来建立“缓冲区—混音器—输出设备”的直觉。下面我按实际拆包和复现的顺序把这份代码的用法、参数和坑一次讲透。2. DirectSound 播放链路拆解从 CoInitialize 到次级缓冲区2.1 为什么是 DirectSound 而不是 WASAPI选型这件事放在今天看DirectSound 的优势只剩三条第一API 数量少核心接口就IDirectSound8和IDirectSoundBuffer8两个新手半天能跑通第二它自带软件混音多个次级缓冲区可以同时播放不需要自己写混音器第三VC 6.0 到 VS2022 都能编译依赖的dsound.lib和dsound.h是 Windows SDK 自带的不需要额外装包。WASAPI 的优势是低延迟和独占模式但代价是事件驱动模型复杂共享模式下的重采样由音频引擎接管你控制不了。XAudio2 更适合游戏但它是 COM 组件初始化链路更长。所以这份代码的定位很清晰不是做专业音频工作站而是做“能稳定出声、能控制播放进度、能循环”的播放器。如果你的场景要求延迟低于 30ms或者需要 ASIO 级别的独占输出那这份代码不适合你直接换 WASAPI 独占模式。2.2 初始化顺序与设备枚举DirectSound 的初始化有一条固定顺序顺序错了就是DSERR_*错误码糊脸。常见做法是先CoInitialize再DirectSoundCreate8然后SetCooperativeLevel最后CreateSoundBuffer。这份代码里把设备枚举单独抽了一个函数用DirectSoundEnumerate回调列出所有输出设备支持用户手动选设备。// 初始化 DirectSound 并设置协作级别 HRESULT InitDirectSound(HWND hWnd, IDirectSound8** ppDS) { HRESULT hr CoInitialize(NULL); // 必须先初始化 COM if (FAILED(hr)) return hr; hr DirectSoundCreate8(NULL, ppDS, NULL); // NULL 表示默认设备 if (FAILED(hr)) return hr; // 协作级别决定优先级普通播放用 DSSCL_NORMAL 即可 hr (*ppDS)-SetCooperativeLevel(hWnd, DSSCL_NORMAL); if (FAILED(hr)) { (*ppDS)-Release(); *ppDS NULL; } return hr; }逻辑说明CoInitialize是 COM 库的入口漏掉它后面所有接口调用都会返回CO_E_NOTINITIALIZED。DirectSoundCreate8的第一个参数传NULL表示用系统默认输出设备如果要指定设备 GUID需要先枚举拿到 GUID 再传进去。SetCooperativeLevel的第二个参数有三个常用值DSSCL_NORMAL适合普通播放不独占设备DSSCL_PRIORITY可以改主缓冲区格式DSSCL_EXCLUSIVE会独占设备其他程序没声音。这份代码用的是DSSCL_NORMAL兼容性最好。参数说明hWnd是窗口句柄必须有效否则SetCooperativeLevel返回DSERR_INVALIDPARAM。如果你在控制台程序里用可以传GetConsoleWindow()但更稳妥的做法是创建一个隐藏窗口。2.3 次级缓冲区的创建与 WAV 数据填充次级缓冲区是 DirectSound 播放的核心。这份代码里创建缓冲区的流程是先解析 WAV 文件头拿到格式和 PCM 数据再用CreateSoundBuffer创建缓冲区最后Lock写入数据、Unlock提交。// 根据 WAV 格式创建次级缓冲区并填充 PCM 数据 HRESULT CreateBufferFromWav(IDirectSound8* pDS, const WAVEFORMATEX* pwfx, BYTE* pcmData, DWORD pcmSize, IDirectSoundBuffer8** ppBuf) { DSBUFFERDESC desc {0}; desc.dwSize sizeof(DSBUFFERDESC); desc.dwFlags DSBCAPS_CTRLVOLUME | DSBCAPS_CTRLPOSITIONNOTIFY | DSBCAPS_GLOBALFOCUS; desc.dwBufferBytes pcmSize; desc.lpwfxFormat (WAVEFORMATEX*)pwfx; // 必须和 WAV 头一致 HRESULT hr pDS-CreateSoundBuffer(desc, (IDirectSoundBuffer**)ppBuf, NULL); if (FAILED(hr)) return hr; void* pWrite1 NULL; DWORD dwLen1 0; void* pWrite2 NULL; DWORD dwLen2 0; // Lock 可能返回两个区域因为缓冲区是环形的 hr (*ppBuf)-Lock(0, pcmSize, pWrite1, dwLen1, pWrite2, dwLen2, 0); if (SUCCEEDED(hr)) { memcpy(pWrite1, pcmData, dwLen1); if (pWrite2) memcpy(pWrite2, pcmData dwLen1, dwLen2); (*ppBuf)-Unlock(pWrite1, dwLen1, pWrite2, dwLen2); } return hr; }逻辑说明DSBUFFERDESC里的dwFlags决定了缓冲区能力。DSBCAPS_CTRLVOLUME允许调音量DSBCAPS_CTRLPOSITIONNOTIFY允许设置播放位置通知DSBCAPS_GLOBALFOCUS让程序失焦后继续播放。dwBufferBytes必须大于等于 PCM 数据大小如果要做流式播放可以设大一点然后分段Lock写入。参数说明pwfx直接指向 WAV 头里的WAVEFORMATEX结构不要自己另建一个否则格式不匹配会导致CreateSoundBuffer返回DSERR_BADFORMAT。Lock的第二个参数是锁定字节数传pcmSize表示锁全量如果缓冲区是环形的pWrite2可能非空必须把第二段也拷贝进去否则播放到尾部会出现杂音或静音。3. 播放控制与线程模型Play、Stop 和位置通知怎么配合3.1 播放、暂停、停止的状态机DirectSound 的播放控制只有三个动作Play、Stop、SetCurrentPosition。但实际用起来暂停不是Stop而是Stop之后记住当前位置恢复时SetCurrentPosition再Play。这份代码里封装了一个简单的状态机用enum标记STOPPED、PLAYING、PAUSED三种状态。// 播放控制暂停时记录位置恢复时跳回 void TogglePause(IDirectSoundBuffer8* pBuf, DWORD* pdwPausePos, bool* pbPaused) { if (*pbPaused) { pBuf-SetCurrentPosition(*pdwPausePos); // 跳回暂停点 pBuf-Play(0, 0, 0); // 继续播放 *pbPaused false; } else { pBuf-GetCurrentPosition(pdwPausePos, NULL); // 记录当前位置 pBuf-Stop(); *pbPaused true; } }逻辑说明GetCurrentPosition的第一个参数是播放游标第二个参数是写游标暂停时只需要读游标。SetCurrentPosition的参数是字节偏移不是毫秒所以如果你要做进度条需要自己按nAvgBytesPerSec换算。Play的第一个参数是保留值必须传 0第二个参数是优先级传 0 表示默认第三个参数是循环标志DSBPLAY_LOOPING表示循环播放。参数说明暂停位置pdwPausePos必须用DWORD保存不要用int因为缓冲区可能超过 2GB虽然实际不会但类型要匹配。如果暂停后设备被其他程序抢占恢复时Play可能返回DSERR_BUFFERLOST这时需要先Restore再重新Lock写入数据。3.2 位置通知与进度条刷新进度条刷新如果靠SetTimer轮询GetCurrentPosition精度差而且浪费 CPU。这份代码用了IDirectSoundNotify8设置位置通知在缓冲区特定偏移处触发事件配合WaitForSingleObject实现精确回调。// 设置位置通知在缓冲区 25%、50%、75% 处触发事件 HRESULT SetupNotify(IDirectSoundBuffer8* pBuf, HANDLE hEvent) { IDirectSoundNotify8* pNotify NULL; HRESULT hr pBuf-QueryInterface(IID_IDirectSoundNotify8, (void**)pNotify); if (FAILED(hr)) return hr; DSBPOSITIONNOTIFY notify[3]; DWORD bufSize 0; pBuf-GetBufferSize(bufSize); for (int i 0; i 3; i) { notify[i].dwOffset bufSize * (i 1) / 4; // 25%, 50%, 75% notify[i].hEventNotify hEvent; } hr pNotify-SetNotificationPositions(3, notify); pNotify-Release(); return hr; }逻辑说明QueryInterface拿到IDirectSoundNotify8后用DSBPOSITIONNOTIFY数组指定偏移和事件句柄。偏移是字节偏移不是百分比所以要自己算。事件句柄用CreateEvent创建手动重置或自动重置都行但要注意在WaitForSingleObject之后根据需求决定是否ResetEvent。参数说明dwOffset不能超过缓冲区大小否则SetNotificationPositions返回DSERR_INVALIDPARAM。如果缓冲区是循环播放的通知会在每次到达偏移时触发适合做循环点标记。注意IDirectSoundNotify8在缓冲区Stop后不会自动清除重新Play前不需要重新设置除非你改了缓冲区大小。3.3 多缓冲区混音与优先级DirectSound 支持同时播放多个次级缓冲区硬件混音器会自动混合。这份代码里虽然只做了一个播放器但你可以创建多个缓冲区同时Play实现背景音乐加音效。优先级通过Play的第二个参数设置值越大优先级越高但实际测试中DSSCL_NORMAL下优先级影响不明显DSSCL_PRIORITY下才有区别。常见做法是背景音乐用一个缓冲区循环播放音效每次触发时创建临时缓冲区播放完释放。但频繁创建释放会导致内存碎片更好的做法是预创建一组缓冲区池用SetCurrentPosition(0)复用。注意DSBCAPS_STATIC标志的缓冲区会占用硬件资源DSBCAPS_LOCHARDWARE强制硬件加速但现代声卡基本都软件模拟了设不设区别不大。4. 避坑与排查设备丢失、格式不匹配和线程死锁4.1 现象播放没声音但函数全返回 S_OK原因最常见的是SetCooperativeLevel传了无效窗口句柄或者窗口被销毁后没有重新设置。另一个原因是 WAV 格式是 24 位或 32 位浮点而 DirectSound 在DSSCL_NORMAL下只保证 16 位 PCM 混音非 16 位格式会被静默拒绝。解决先用DirectSoundCaptureEnumerate确认设备存在再检查WAVEFORMATEX的wBitsPerSample是否为 16。如果是 24 位用mmio或手动降位到 16 位再创建缓冲区。窗口句柄用GetSafeHwnd()或GetConsoleWindow()确保有效。4.2 现象播放到一半卡住GetCurrentPosition 不再变化原因缓冲区被Lock后没有Unlock或者Unlock的参数和Lock返回的不一致。DirectSound 的Lock是临界区操作未解锁会导致后续Play阻塞。解决检查所有Lock路径确保Unlock在同一个函数内调用且pWrite1/dwLen1和pWrite2/dwLen2原样传回。如果用了异常处理在catch里也要Unlock。我一般会封装一个 RAII 类析构时自动Unlock。4.3 现象切换设备后播放失败返回 DSERR_BUFFERLOST原因音频设备被拔出或禁用DirectSound 缓冲区丢失。DSSCL_NORMAL下系统不会自动恢复必须手动Restore。解决在Play返回DSERR_BUFFERLOST时调用Restore()然后重新Lock写入 PCM 数据再Play。注意Restore后缓冲区内容全丢必须重新填充。如果设备已经不存在需要重新枚举设备并重建缓冲区。4.4 现象程序退出时崩溃堆栈在 CoUninitialize原因CoUninitialize调用时还有 DirectSound 接口未释放或者释放顺序错了。COM 对象必须在CoUninitialize之前全部Release。解决按创建顺序逆序释放先释放所有次级缓冲区再释放IDirectSound8最后CoUninitialize。如果用了智能指针注意Release的调用时机。这份代码里在OnDestroy里统一清理顺序不能乱。4.5 现象多线程下播放位置通知事件丢失原因IDirectSoundNotify8的事件句柄在多个线程里WaitForSingleObject但事件是自动重置的一个线程消费后另一个线程就等不到。解决每个通知偏移用独立的事件句柄或者用CreateEvent(NULL, TRUE, FALSE, NULL)创建手动重置事件在WaitForSingleObject后手动ResetEvent。如果只是刷新进度条建议在主线程用PeekMessage或SetTimer轮询避免多线程复杂度。5. 进阶技巧用 mmio 解析 WAV 并做流式播放5.1 为什么不用现成解码库这份代码只支持 WAV因为 WAV 是未压缩 PCM解析头就能直接送进缓冲区。MP3 和 AAC 需要解码器引入ffmpeg或libmad会让工程体积翻倍。如果你的场景必须支持 MP3常见做法是用Media Foundation的SourceReader解码成 PCM再喂给 DirectSound。但那样就不如直接用 WASAPI 了。所以这份代码的边界很清晰WAV 播放器适合做提示音、报警音、简单背景音乐。如果你要做一个完整的音乐播放器建议把 DirectSound 当作输出层上面接一个解码层解码层输出 PCM 块DirectSound 用流式缓冲区循环写入。5.2 流式播放的缓冲区管理流式播放的核心是双缓冲或环形缓冲。创建一个较大的次级缓冲区比如 2 秒时长然后用IDirectSoundNotify8在播放到一半时触发事件事件里Lock后半段写入下一块 PCM 数据。这样播放不中断内存占用也可控。// 流式播放在通知事件里填充后半段缓冲区 void OnNotifyFillBuffer(IDirectSoundBuffer8* pBuf, FILE* fpWav, DWORD dwNotifyOffset, DWORD dwBufSize) { DWORD dwPlay 0, dwWrite 0; pBuf-GetCurrentPosition(dwPlay, dwWrite); // 计算要填充的区域从写游标开始填到播放游标之前 DWORD dwFillStart dwWrite; DWORD dwFillSize (dwPlay dwWrite) ? (dwPlay - dwWrite) : (dwBufSize - dwWrite dwPlay); if (dwFillSize 0) return; void* p1 NULL; DWORD len1 0; void* p2 NULL; DWORD len2 0; if (SUCCEEDED(pBuf-Lock(dwFillStart, dwFillSize, p1, len1, p2, len2, 0))) { fread(p1, 1, len1, fpWav); if (p2) fread(p2, 1, len2, fpWav); pBuf-Unlock(p1, len1, p2, len2); } }逻辑说明GetCurrentPosition拿到播放和写游标写游标是 DirectSound 内部维护的表示下一次Lock的推荐位置。填充区域是写游标到播放游标之间的空闲区。如果dwPlay dwWrite说明播放游标在前空闲区是连续的否则空闲区跨越缓冲区尾部需要分两段Lock。参数说明dwFillSize不能超过缓冲区大小也不能小于一个采样块的大小比如 4KB否则会频繁触发通知。fread的返回值要检查如果读到文件尾可以选择循环rewind或者停止播放。注意Lock的偏移和大小必须是块对齐的否则可能返回DSERR_INVALIDPARAM。5.3 验证播放是否正常的三个手段第一个手段用GetStatus检查缓冲区状态DSBSTATUS_PLAYING表示正在播放DSBSTATUS_BUFFERLOST表示丢失。第二个手段用GetCurrentPosition打印游标正常播放时游标应该单调递增暂停时不变。第三个手段用示波器或录音软件录下输出看波形是否连续有没有断点或爆音。我一般会在OnTimer里每 500ms 打印一次GetCurrentPosition和GetStatus如果游标不动或者状态异常基本能定位到是缓冲区丢失还是线程阻塞。从那以后我每次接手 DirectSound 工程都强制先跑一遍设备枚举和格式检查确认 16 位 PCM 再往下写。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑