VC++项目背景音乐实现:Windows原生API与MCI实战指南
1. 项目概述为什么要在VC项目中添加背景音乐在桌面应用开发尤其是游戏、多媒体工具或者一些需要氛围感的工具软件里背景音乐BGM是一个能显著提升用户体验的“软实力”。很多刚接触VC这里特指使用Microsoft Visual C进行Win32或MFC桌面程序开发的朋友可能会觉得播放个音乐是个复杂的事情得依赖DirectX或者第三方库。其实不然Windows平台本身就提供了强大且易用的多媒体API足以应对大多数背景音乐播放的需求。我最近在维护一个老的项目里面就有背景音乐模块正好结合实战把从原理到踩坑的完整过程梳理一遍。这个实战指南的核心就是教你如何在不引入庞大第三方库的前提下使用Windows原生的PlaySound和更强大的MCI媒体控制接口API在VC项目中实现可靠、可控的背景音乐播放。我们会从最简单的“滴”一声开始逐步深入到循环播放、音量控制、暂停与恢复甚至解决一个最近被频繁搜索的问题——“obs背景音乐断断续续”。虽然OBS是另一个软件但其底层音频处理机制与我们直接相关理解它有助于我们写出更健壮的代码。无论你是想给一个小工具添加点提示音还是为你的游戏项目配上循环BGM这篇指南都能提供可直接“抄作业”的解决方案。2. 核心方案选型PlaySound vs. MCI在Windows C环境下播放音频主要有几个选择古老的PlaySound、功能全面的MCI、专业的DirectSound以及现代的Core AudioAPI。对于背景音乐这种需求我们主要在前两者之间做选择。2.1 PlaySound简单场景的利器PlaySound函数位于windows.h和mmsystem.h中使用前需要链接winmm.lib库。它的特点是极其简单一行代码就能播放WAV文件。#include windows.h #include mmsystem.h #pragma comment(lib, winmm.lib) // 播放系统警告声 PlaySound(TEXT(SystemAsterisk), NULL, SND_ALIAS | SND_ASYNC); // 播放指定WAV文件 PlaySound(TEXT(bgm.wav), NULL, SND_FILENAME | SND_ASYNC);它的标志位Flag很关键SND_FILENAME: 参数是文件名。SND_ASYNC: 异步播放函数调用立即返回不会阻塞主线程。这是播放背景音乐必须的选项否则界面会卡住直到播放完毕。SND_LOOP: 循环播放。但注意SND_LOOP必须和SND_ASYNC一起使用。SND_RESOURCE: 从程序的资源文件.rc中播放WAV资源。SND_NODEFAULT: 如果找不到指定声音不播放默认系统声音。实战心得与局限PlaySound的优势是简单但它基本上只支持WAV格式某些系统别名可能对应其他格式但不可靠。最大的问题是控制力弱。你很难在播放中途去精确控制它的音量、暂停、播放进度。虽然可以用NULL作为文件名调用PlaySound来停止当前由它播放的声音但这会停止所有由它播放的异步声音控制粒度太粗。因此PlaySound更适合播放短暂的音效对于需要交互控制的背景音乐就显得力不从心了。2.2 MCI功能全面的选择MCIMedia Control Interface是一个高级的、命令字符串驱动的多媒体API。它像一个统一的遥控器可以控制CD音频、数字音频WAV、MIDI、AVI视频等多种设备。它的功能比PlaySound强大得多。为什么选择MCI格式支持广通过不同的设备类型可以支持WAV、MP3、MIDI等。对于MP3通常使用MPEGVideo或MPEGAudio设备类型。控制精细可以轻松实现播放、暂停、继续、停止、跳转到指定位置、循环播放、音量调节等。状态查询可以查询当前播放状态、播放位置、媒体长度等信息。资源友好可以将音频文件嵌入程序资源从内存中播放便于发布单文件程序。它的主要接口是mciSendString使用字符串命令和mciSendCommand使用消息结构。对于初学者mciSendString更直观易懂本指南将主要以此为例。同样需要包含mmsystem.h并链接winmm.lib。#include windows.h #include mmsystem.h #pragma comment(lib, winmm.lib) // 使用mciSendString播放 MCIERROR err mciSendString(TEXT(open \bgm.mp3\ alias mymusic), NULL, 0, NULL); if (err 0) { mciSendString(TEXT(play mymusic), NULL, 0, NULL); }方案取舍结论 如果你的背景音乐只是简短的WAV音效且不需要中途控制用PlaySound是最快的。但绝大多数“背景音乐”场景都意味着需要循环、可能需要暂停/继续、甚至调节音量。因此本指南后续将聚焦于使用MCI API来实现一个健壮的背景音乐管理器。这是更通用、更专业的做法。3. 实战构建一个健壮的背景音乐管理器类直接在各处调用mciSendString会导致代码混乱不易管理。封装成一个类是最佳实践。下面我们一步步构建一个BackgroundMusic类。3.1 类的设计与初始化首先我们设计这个类应该有的功能打开音频文件、播放、暂停、继续、停止、设置循环、设置音量、关闭设备。我们使用std::wstring来存储设备别名和文件路径以更好地支持Unicode。// BackgroundMusic.h #pragma once #include string #include windows.h #include mmsystem.h class BackgroundMusic { public: BackgroundMusic(); ~BackgroundMusic(); // 打开音频文件支持文件路径或资源ID bool Open(const std::wstring filePath, bool fromResource false); // 播放 (loop参数决定是否循环) bool Play(bool loop false); // 暂停 bool Pause(); // 继续播放 bool Resume(); // 停止 bool Stop(); // 关闭设备释放资源 void Close(); // 设置音量 (0 ~ 1000) bool SetVolume(int volume); // 是否正在播放 bool IsPlaying() const; // 从程序资源中打开 bool OpenFromResource(int resourceID, const std::wstring resourceType LWAVE); private: std::wstring m_deviceAlias; // MCI设备别名 bool m_isOpened; // 设备是否已打开 bool m_isPlaying; // 是否正在播放状态 };在构造函数中我们生成一个唯一的设备别名避免多个实例冲突。这里使用时间戳和随机数来生成。// BackgroundMusic.cpp #include BackgroundMusic.h #include sstream #include chrono #include random BackgroundMusic::BackgroundMusic() : m_isOpened(false), m_isPlaying(false) { // 生成一个唯一的设备别名例如 BGM_1630423456123 auto now std::chrono::system_clock::now(); auto timestamp std::chrono::duration_caststd::chrono::milliseconds(now.time_since_epoch()).count(); std::mt19937 rng(static_castunsigned int(timestamp)); std::uniform_int_distributionint dist(1000, 9999); int random dist(rng); std::wstringstream wss; wss LBGM_ timestamp L_ random; m_deviceAlias wss.str(); } BackgroundMusic::~BackgroundMusic() { Close(); // 析构时确保关闭设备 }3.2 核心方法实现打开与播放Open方法是核心它负责向MCI发送open命令。这里有一个关键细节MCI的open命令对于包含空格或特殊字符的文件路径需要用引号括起来。我们的代码需要处理这种情况。bool BackgroundMusic::Open(const std::wstring filePath, bool fromResource) { if (m_isOpened) { Close(); // 如果已经打开了一个先关闭 } std::wstring openCmd; if (fromResource) { // 从资源打开格式: open type [资源类型] alias [别名] // 注意资源播放需要文件在资源中正确定义且类型为WAVE等 // 这里简化处理实际应用需结合OpenFromResource return false; } else { // 处理文件路径中的空格如果路径包含空格确保整个路径被双引号包裹 std::wstring formattedPath filePath; // 简单的判断如果路径中有空格且没有被引号包围则加上引号 if (filePath.find(L ) ! std::wstring::npos filePath.front() ! L\ filePath.back() ! L\) { formattedPath L\ filePath L\; } openCmd Lopen formattedPath L type mpegvideo alias m_deviceAlias; // 注意type mpegvideo 常用于MP3。对于WAV文件可以尝试 type waveaudio 或直接省略type。 // 更通用的做法是让调用者指定type或者根据文件扩展名自动判断。 // 这里我们做一个简单的自动判断 size_t dotPos filePath.rfind(L.); if (dotPos ! std::wstring::npos) { std::wstring ext filePath.substr(dotPos 1); // 转换为小写比较 std::transform(ext.begin(), ext.end(), ext.begin(), ::towlower); if (ext Lwav) { openCmd Lopen formattedPath L type waveaudio alias m_deviceAlias; } else if (ext Lmid || ext Lmidi) { openCmd Lopen formattedPath L type sequencer alias m_deviceAlias; } else { // 默认为MPEGVideo对MP3支持较好 openCmd Lopen formattedPath L type mpegvideo alias m_deviceAlias; } } } MCIERROR err mciSendString(openCmd.c_str(), NULL, 0, NULL); if (err ! 0) { // 可以调用mciGetErrorString获取错误信息便于调试 wchar_t errorBuf[256]; mciGetErrorString(err, errorBuf, 256); // 输出或记录错误信息 OutputDebugString(errorBuf); return false; } m_isOpened true; m_isPlaying false; return true; }注意事项设备类型type这是最容易出错的地方之一。对于MP3文件使用type mpegvideo或type mpegaudio通常有效。对于WAV文件使用type waveaudio。如果省略typeMCI会尝试自动检测但有时会失败。明确指定类型更可靠。别名alias我们为每个打开的音频设备指定了一个唯一别名后续所有操作play, pause, stop, setaudio都通过这个别名来指向具体的设备。错误处理一定要检查mciSendString的返回值MCIERROR。使用mciGetErrorString可以将错误码转换为可读信息这在调试时至关重要。接下来是Play方法它需要处理循环播放。bool BackgroundMusic::Play(bool loop) { if (!m_isOpened) return false; std::wstring playCmd Lplay m_deviceAlias; if (loop) { playCmd L repeat; } MCIERROR err mciSendString(playCmd.c_str(), NULL, 0, NULL); if (err 0) { m_isPlaying true; return true; } return false; }3.3 控制与状态查询暂停、继续、停止与音量暂停和继续分别使用pause和resume命令。注意resume并非对所有设备类型都有效有时可能需要用play命令来继续。更通用的做法是记录状态。bool BackgroundMusic::Pause() { if (!m_isOpened || !m_isPlaying) return false; std::wstring cmd Lpause m_deviceAlias; if (mciSendString(cmd.c_str(), NULL, 0, NULL) 0) { m_isPlaying false; // 更新内部状态 return true; } return false; } bool BackgroundMusic::Resume() { if (!m_isOpened || m_isPlaying) return false; // 如果已经在播放就不执行 std::wstring cmd Lresume m_deviceAlias; // 先尝试resume if (mciSendString(cmd.c_str(), NULL, 0, NULL) ! 0) { // 如果resume失败尝试用play继续可能需要seek到当前位置这里简化处理 // 一个更精确的做法是在Pause时记录位置Resume时先seek再play cmd Lplay m_deviceAlias; } if (mciSendString(cmd.c_str(), NULL, 0, NULL) 0) { m_isPlaying true; return true; } return false; }停止和关闭设备bool BackgroundMusic::Stop() { if (!m_isOpened) return false; std::wstring cmd Lstop m_deviceAlias; if (mciSendString(cmd.c_str(), NULL, 0, NULL) 0) { m_isPlaying false; // 通常stop后会seek到开始 mciSendString((Lseek m_deviceAlias L to start).c_str(), NULL, 0, NULL); return true; } return false; } void BackgroundMusic::Close() { if (m_isOpened) { Stop(); // 先停止播放 std::wstring cmd Lclose m_deviceAlias; mciSendString(cmd.c_str(), NULL, 0, NULL); m_isOpened false; m_isPlaying false; } }音量控制是背景音乐交互的关键。MCI使用setaudio命令来设置音量音量范围通常是0静音到1000最大。但这里有一个巨大的坑setaudio命令并非对所有音频设备或所有Windows版本都有效尤其是对于mpegvideo设备类型的MP3文件。更现代、更可靠的方法是使用waveOutSetVolume等API但这涉及更底层的操作。一个相对兼容性较好的方法是使用MCI_SET命令。但经过大量测试我发现最普适有效的方案是使用setaudio命令并确保在open命令中不指定type或者使用type waveaudio对于WAV文件这样音量控制成功率更高。对于MP3可以尝试在open后先设置一下音频为“波形音频”模式。bool BackgroundMusic::SetVolume(int volume) { if (!m_isOpened) return false; // 确保音量在0~1000范围内 int vol (volume 0) ? 0 : ((volume 1000) ? 1000 : volume); std::wstringstream wss; wss Lsetaudio m_deviceAlias L volume to vol; MCIERROR err mciSendString(wss.str().c_str(), NULL, 0, NULL); // 如果上述方法失败尝试另一种命令格式有些文档提到用“audio” if (err ! 0) { wss.str(L); wss.clear(); wss Lset m_deviceAlias L audio volume to vol; err mciSendString(wss.str().c_str(), NULL, 0, NULL); } return (err 0); }实操心得 音量控制失败是MCI使用中最常见的问题之一。如果setaudio始终无效可以考虑以下备选方案使用PlaySound播放WAV并通过混音器APImixerSetControlDetails调节进程音量但这比较复杂。升级到更现代的音频API如DirectSound或Core AudioIAudioEndpointVolume它们提供了更精确的控制但复杂度也更高。对于背景音乐一个取巧的办法是如果音量控制只是为了开关可以用打开/关闭设备来代替有延迟。或者在音频编辑软件中预先处理好不同音量的版本运行时切换文件。3.4 从资源文件播放音频将音频文件嵌入程序资源可以避免发布时附带一堆外部文件使程序更整洁。首先需要在资源文件.rc中添加资源// 在resource.rc或.rc2文件中 IDR_BGM_MUSIC WAVE res\\background.mp3然后在代码中使用OpenFromResource方法打开。注意MCI的open命令对于资源语法是open type [资源类型] [资源标识] alias [别名]。资源标识可以是资源ID数字或名称字符串。bool BackgroundMusic::OpenFromResource(int resourceID, const std::wstring resourceType) { if (m_isOpened) Close(); // 注意对于资源路径格式比较特殊。通常需要 #id 或文件名。 // 一种常见做法是将资源临时提取到内存或临时文件然后用文件方式打开。 // 这里介绍另一种MCI直接支持的方式对WAVE资源支持较好 std::wstringstream wss; wss Lopen \# resourceID L\ type resourceType L alias m_deviceAlias; MCIERROR err mciSendString(wss.str().c_str(), NULL, 0, NULL); if (err ! 0) { // 如果上述方法失败尝试另一种格式使用资源名称 // 假设资源名称为字符串需要提前在resource.h中定义 // 例如 #define IDR_BGM_MUSIC_STR LBGM_MUSIC // 命令为open \#BGM_MUSIC\ type WAVE alias mymusic // 由于资源名获取较复杂此处不展开。实战中更推荐使用“提取到临时文件”的方案兼容性最好。 return false; } m_isOpened true; m_isPlaying false; return true; }重要提醒直接从资源播放MP3type mpegvideo可能不被所有系统支持。最稳妥的跨格式方案是在程序启动时将资源中的音频文件提取到临时目录的一个临时文件中然后使用普通的Open(临时文件路径)方法打开。播放结束后或程序退出时删除这个临时文件。这样你享受了资源文件的整洁又获得了文件播放的兼容性。4. 集成到MFC或Win32项目消息循环与资源管理4.1 在MFC对话框中的应用假设你有一个MFC对话框程序上面有几个按钮播放、暂停、停止、循环、音量滑块。首先在对话框类的头文件中包含我们的BackgroundMusic类并声明一个成员变量。// MyDialog.h #include BackgroundMusic.h class CMyDialog : public CDialogEx { // ... private: BackgroundMusic m_bgm; // ... };在对话框的初始化OnInitDialog中打开音频文件。你可以选择打开外部文件或资源文件。BOOL CMyDialog::OnInitDialog() { CDialogEx::OnInitDialog(); // 方法1打开外部文件 if (!m_bgm.Open(L.\\music\\theme.mp3)) { AfxMessageBox(L无法打开背景音乐文件); } // 方法2打开资源假设已按前述方法将MP3加入资源 // if (!m_bgm.OpenFromResource(IDR_BGM_MUSIC, LMPEGVideo)) { ... } // 设置初始音量比如50% m_bgm.SetVolume(500); return TRUE; }为按钮添加事件处理程序void CMyDialog::OnBnClickedButtonPlay() { // 从“循环播放”复选框获取状态 CButton* pCheck (CButton*)GetDlgItem(IDC_CHECK_LOOP); bool loop (pCheck-GetCheck() BST_CHECKED); m_bgm.Play(loop); } void CMyDialog::OnBnClickedButtonPause() { if (m_bgm.IsPlaying()) { m_bgm.Pause(); SetDlgItemText(IDC_BUTTON_PAUSE, L继续); // 改变按钮文字 } else { m_bgm.Resume(); SetDlgItemText(IDC_BUTTON_PAUSE, L暂停); } } void CMyDialog::OnBnClickedButtonStop() { m_bgm.Stop(); SetDlgItemText(IDC_BUTTON_PAUSE, L暂停); // 重置按钮文字 } // 音量滑块IDC_SLIDER_VOLUME的事件处理 void CMyDialog::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { if (pScrollBar-GetDlgCtrlID() IDC_SLIDER_VOLUME) { CSliderCtrl* pSlider (CSliderCtrl*)pScrollBar; int pos pSlider-GetPos(); // 假设滑块范围是0~100 int volume pos * 10; // 映射到0~1000 m_bgm.SetVolume(volume); } CDialogEx::OnHScroll(nSBCode, nPos, pScrollBar); }在对话框关闭时OnDestroy或析构函数中记得关闭音乐设备。void CMyDialog::OnDestroy() { m_bgm.Close(); // 确保资源被释放 CDialogEx::OnDestroy(); }4.2 在Win32窗口程序中的应用在纯Win32 API项目中逻辑是类似的。在WinMain中或窗口创建后初始化BackgroundMusic对象在窗口过程WndProc中响应按钮点击如WM_COMMAND和滑块消息如WM_HSCROLL调用相应的方法。关键点在于必须确保音频操作在主线程或同一个线程中。MCI命令虽然是异步的但设备句柄是线程相关的。在一个线程中打开的设备最好在同一个线程中控制它。通常放在主UI线程里操作是最简单的。资源管理注意事项一个设备一个实例一个BackgroundMusic对象管理一个MCI设备。不要用同一个对象重复Open不同文件而不Close这会导致资源泄漏。及时关闭在程序退出或不再需要播放时务必调用Close()。类的析构函数中调用是很好的做法。错误处理在生产代码中所有mciSendString的调用都应该检查返回值并记录日志尤其是在Open和SetVolume时。这能帮助快速定位“为什么没声音”的问题。5. 高级议题与疑难杂症排查5.1 解决“背景音乐断断续续”问题这个问题在网络搜索中很常见尤其是在OBS等录制或直播软件中出现。其根本原因通常是音频缓冲区不足或系统音频处理线程被阻塞。在我们的VC程序中也可能遇到尤其是当主线程忙于大量计算或UI渲染没有及时处理消息队列时。MCI播放本质上是依赖Windows的消息机制和后台线程。如果UI线程被长时间阻塞音频数据的供给就可能不及时导致卡顿、断续。以下是排查和解决步骤检查主线程是否阻塞在播放音乐时避免在主线程执行耗时操作如大文件读写、复杂计算循环。将这些操作移到工作线程。使用更稳定的音频APIMCI毕竟是较老的API。如果对音频流畅性要求高可以考虑使用DirectSound或Windows Media Foundation (WMF)。DirectSound提供了更底层的缓冲区控制可以设置更大的缓冲区来对抗偶尔的延迟。提升播放线程优先级虽然MCI内部有自己的线程但我们无法直接控制。使用DirectSound可以创建独立的播放线程并设置其优先级。优化文件读取如果音频文件很大或者是从网络驱动器、慢速USB设备读取可能会造成断续。尝试将文件预加载到内存缓冲区然后从内存播放。MCI支持open命令的buffer参数但更通用的做法是使用mmioOpen等API将文件读入内存然后指定内存地址打开。系统音频驱动问题更新声卡驱动程序。有时某些版本的驱动与MCI兼容性不佳。检查其他音频软件冲突关闭其他可能独占音频设备的软件如某些音乐播放器、通讯软件。一个简单的MCI内存播放示例思路// 1. 将音频文件读入内存缓冲区 (hMem) // 2. 使用 mmioOpen 等API以内存模式打开 // 3. MCI open 命令: Lopen [内存缓冲区标识] type [类型] alias [别名] // 具体实现较为复杂涉及MMIO API此处不展开。这通常用于对延迟极其敏感的场景。对于大多数桌面背景音乐应用确保主线程不长时间阻塞并使用本地磁盘上的文件基本可以避免断续问题。5.2 多音乐管理与混音如果你需要同时播放多个背景音乐比如环境音角色语音或者实现混音同时播放多个音源简单的MCI可能不够用。因为每个MCI设备通常独占音频输出。你可以尝试打开多个MCI设备使用不同别名然后同时播放。但混音效果和性能取决于系统音频驱动。更专业的方案是使用DirectSound。它可以创建多个次级声音缓冲区IDirectSoundBuffer由IDirectSound主对象进行混音后输出。这给了你完全的控制权可以实现3D音效、动态混音等高级功能。当然复杂度也呈指数级上升。5.3 常见MCI错误码速查当mciSendString返回非零值时使用mciGetErrorString获取错误描述。以下是一些常见错误及可能原因错误描述可能错误原因与排查方向MMSYSERR_INVALPARAM参数无效。检查命令字符串格式特别是文件路径的引号、别名是否包含空格。MCIERR_CANNOT_LOAD_DRIVER无法加载驱动程序。指定的设备类型如mpegvideo系统不支持。尝试更换类型或安装对应的解码器如LAME MP3 codec。MCIERR_DEVICE_NOT_READY设备未就绪。可能设备已被占用或文件正在被其他程序锁定。MCIERR_FILE_NOT_FOUND文件未找到。检查路径是否正确工作目录是否如预期。使用绝对路径更可靠。MCIERR_DEVICE_NOT_INSTALLED设备未安装。系统缺少必要的多媒体组件。MCIERR_OUT_OF_MEMORY内存不足。音频文件太大或系统资源紧张。调试技巧在开发阶段可以将每次发送的MCI命令字符串和返回的错误信息输出到调试窗口OutputDebugString或日志文件这是快速定位问题的利器。5.4 关于“电脑vc库自检”的关联思考网络热词“电脑vc库自检”通常指的是程序运行时缺少特定版本的Visual C Redistributable运行时库。我们的程序如果使用了MFC并且动态链接到MFC库那么目标机器上就必须安装对应版本的VC运行库。虽然winmm.lib是系统库一般不存在这个问题但你的项目如果编译为Release版本并分发给他人务必考虑运行库依赖。发布建议使用静态链接到MFC和运行时库在项目属性 - 配置属性 - 常规 - MFC的使用中选择“在静态库中使用MFC”在C/C - 代码生成 - 运行时库中选择“多线程(/MT)”。这样生成的exe文件更大但可以独立运行无需安装运行库。如果使用动态链接则必须随程序分发或要求用户安装对应版本的Microsoft Visual C Redistributable安装包。通常可以在微软官网下载。6. 性能优化与扩展思路6.1 预加载与延迟播放对于需要快速响应的音效如游戏中的枪声可以使用预加载技术。在程序初始化时就打开音频设备但不播放open命令。当需要播放时直接发送play命令这会比临时执行open再play快得多。当然这会占用更多的系统资源设备句柄。你需要根据音频的数量和内存权衡。// 初始化阶段 m_bgm.Open(Lsound.wav); // ... 需要播放时 m_bgm.Play(false);6.2 使用事件通知MCI支持在播放完成时发送通知消息notify标志。这可以用来实现“播放结束后自动播放下一首”之类的功能。// 在play命令中加入 notify std::wstring cmd Lplay m_deviceAlias L notify; mciSendString(cmd.c_str(), NULL, 0, hWnd); // 需要传递窗口句柄hWnd然后在你的窗口过程中处理MM_MCINOTIFY消息。case MM_MCINOTIFY: if (wParam MCI_NOTIFY_SUCCESSFUL) { // 播放成功结束触发下一首或其它操作 PostMessage(hWnd, WM_USER_PLAY_NEXT, 0, 0); } break;6.3 向现代API迁移的考量虽然MCI简单易用但它是Windows 95时代的技术微软已不再积极更新。对于新的项目尤其是对音频质量、延迟、功能有较高要求的可以考虑学习以下更现代的APIWindows Media Foundation (WMF)这是微软推荐的现代多媒体API支持格式广泛硬件加速但学习曲线较陡峭。DirectSound已弃用但广泛支持相比MCI控制更精细延迟更低适合游戏开发。但微软已将其标记为“legacy”未来可能移除。Core Audio APIs如WASAPI这是Windows Vista之后最底层的音频API提供了最低的延迟和最大的控制权但复杂度最高。对于大多数“添加背景音乐”的需求MCI在可预见的未来仍然是够用且最快捷的选择。它的优势在于几乎100%的Windows兼容性和极低的入门门槛。当你需要更复杂的功能时再考虑迁移也不迟。最后我个人的体会是技术选型没有绝对的好坏只有适合与否。在VC项目中添加背景音乐从PlaySound的一行代码到MCI的精细控制再到考虑DirectSound是一个根据需求逐步深入的过程。最关键的是理解每种方法背后的原理和限制这样无论遇到“断断续续”还是“没有声音”的问题你都能有条不紊地找到根源并解决它。把上面封装的BackgroundMusic类用起来它已经能解决80%的背景音乐需求了。如果在使用过程中发现需要循环播放列表、淡入淡出效果那可能就是你需要探索DirectSound或BASS这样的专业音频库的时候了。