使用 Windows 多媒体库播放 .WAV 文件






4.67/5 (56投票s)
2004年3月26日
20分钟阅读

427474

12897
这是一个使用 Windows 多媒体库的简单程序
引言
我注意到,教程的音频部分是一个很难写的章节。所有为这个章节撰写的文章都收到了非常糟糕的评论。我预计,一旦大家意识到这是关于什么的,这篇文章也会收到同样的评价。
旧的 Windows 多媒体扩展库,即 WINMM.DLL。本教程不会讨论 Direct Sound 或任何其他新的音频系统。在本教程中,我们将学习什么是声音,以及如何使用 WINMM.DLL 播放声音。事实上,我们将编写一个库,以便在我们的代码中轻松使用它来播放声音!
请注意,我通常不是音频方面的人,我对这个主题了解不多。这篇文章不适合那些想自己编写 DSP 的硬核音频爱好者。这篇文章是写给我们其他人的!
什么是 CODEC?
CODEC 是 COmpression DECompression(压缩解压缩)的缩写。CODEC 知道如何压缩和解压缩给定的格式。虽然 CODEC 通常在视频和音频的上下文中被提及,但它们并不局限于此范围。
什么是音频编码器驱动程序?
您可能听说过或见过名为“ACM”(音频转换器模块)的文件。这些是安装在您系统上的驱动程序,它们导出了可以对特定音频流执行的方法。这包括将音频从一种流类型转换为另一种。一个例子可能是 MP3 ACM,它可以将 MP3 音频转换为 PCM 音频。
构成音频流的属性有哪些?
“每样本位数”、“通道数”和“采样率”。“每样本位数”是指声音的每个样本中有多少位。“通道数”可以看作是进程中的线程。它们基本上是包含在一个流中的独立音频流,同时播放。您通常称之为“立体声”、“单声道”和“环绕声”。采样率是音频播放的速度。采样率越低,每个样本的播放时间越长。这显然会降低质量,但可以节省空间。采样率越高,每个样本的播放速度越快,提高质量并消耗更多空间。
采样率是每秒采集的样本数。如果采样率为“8000 Hz”,则表示每秒 8000 个样本(这通常也以千赫兹表示,即 8 KHz)。如果采样率为 44,100 Hz,则表示每秒 44,100 个样本。让我们来计算一下。
如果我们以 44,100 Hz 播放音频,并且音频是 16 位,通道数为 2,那么每秒播放多少字节?
(44100 * 16 * 2)/8 = 176400 Bytes Per Second
所以,公式是
(Hz * Sample Bit Rate * Channels)/(Bits Per Byte) = Bytes Per Second
为了描述这些设置,Windows 多媒体扩展定义了这个音频结构
/* * extended waveform format structure used for all non-PCM formats. this * structure is common to all non-PCM formats. */ typedef struct tWAVEFORMATEX { WORD wFormatTag; /* format type */ WORD nChannels; /* number of channels (i.e. mono, stereo...) */ DWORD nSamplesPerSec; /* sample rate */ DWORD nAvgBytesPerSec; /* for buffer estimation */ WORD nBlockAlign; /* block size of data */ WORD wBitsPerSample; /* number of bits per sample of mono data */ WORD cbSize; /* the count in bytes of the size of */ /* extra information (after cbSize) */ } WAVEFORMATEX, *PWAVEFORMATEX, NEAR *NPWAVEFORMATEX, FAR *LPWAVEFORMATEX;
忽略关于“PCM”的顶部注释,唯一的区别是结构中添加了 cbSize
作为最后一个值。PCM 是我们将在本次演示中讨论的内容。
什么是 PCM?
PCM 是脉冲编码调制 (Pulse Code Modulation)。这基本上是一种“原始”音频格式。这通常是音频硬件直接交互的格式。虽然某些硬件可以直接播放其他格式,但通常软件必须将任何音频流转换为 PCM,然后尝试播放。
模拟音频流通常看起来像一个波形,有峰值和谷值,称为其幅度。这些通常被发送到音频硬件,然后被数字化。转换基本上以给定的频率(例如 8000 Hz 等)对该数据流进行采样。这种采样通常会测量每秒通过的电压次数,并根据此生成一个值。请记住,我不是音频工程师,所以这只是一个简化的思考方式。例如,PCM 格式,我认为基线是 127 或 128(在每样本 8 位的情况下)。这是中间值,即静音。低于此值是低音调,高于此值是高音调。如果您获取这些值的集合并以特定速度播放它们,它们将发出声音。
那么它是如何工作的?
在 Windows 中,我们通常使用几个函数来创建音频流。我们需要使用 waveOutOpen()
打开音频输出设备,并指定我们要使用的音频格式。这将查询硬件以确定是否可以使用此格式。如果可以,我们将获得设备句柄,然后就可以开始了。如果不能,我们需要选择另一种格式。
选择其他格式?
通常,如果您不请求 PCM 的变体,您将无法打开波形设备。那么,如何播放其他音频格式?很简单。将它们转换为 PCM。您无需知道如何将音频格式转换为 PCM,甚至无需了解该格式的任何信息。您只需要确保系统安装了执行此操作的编码器,然后执行转换。您需要知道的是如何读取音频存储在其中的文件格式!
如何使用编码器进行转换?
您只需使用 Acm* API。这些 API 允许您打开一个编码器并将其转换为另一个编码器。前提是该编码器支持转换为其他编码器!如果不支持,您就必须找到另一个编码器。例如,假设 MP3 可以转换为 GSM,而 GSM 可以转换为 PCM(这并非事实,只是一个示例)。因此,您将 MP3 转换为 GSM,然后将 GSM 转换为 PCM。这也可以在比编码器更详细的层面上实现。也许 MP3 只支持转换为 16 位 PCM。因此,您必须使用 MP3 编码器将其转换为 16 位 PCM,然后使用 PCM 编码器将其转换为 8 位 PCM。这只是一个例子。
您无需担心系统中安装了哪些编码器,只需调用 API 即可,系统将负责加载它们。如果它们不在系统中,您将收到一条错误消息。本教程不会介绍如何创建编码器驱动程序,也不会介绍 Acm* API。
系统如何找到编码器?
编码器在注册表中注册。每个编码器,在 Microsoft 结构中称为“格式标签”(format tag),实际上是一个已注册的值!这意味着如果您想出自己的格式并希望它(永远地、跨平台地、且不产生冲突地)工作,您就需要向 Microsoft 注册您的格式。Microsoft 然后会给您一个数字,并将您的格式添加到其头文件中。当前注册的格式列在 MMREG.H 中。这是一个摘录。
/* WAVE form wFormatTag IDs */ #define WAVE_FORMAT_UNKNOWN 0x0000 /* Microsoft Corporation */ #define WAVE_FORMAT_ADPCM 0x0002 /* Microsoft Corporation */ #define WAVE_FORMAT_IEEE_FLOAT 0x0003 /* Microsoft Corporation */ #define WAVE_FORMAT_VSELP 0x0004 /* Compaq Computer Corp. */ #define WAVE_FORMAT_IBM_CVSD 0x0005 /* IBM Corporation */ #define WAVE_FORMAT_ALAW 0x0006 /* Microsoft Corporation */ #define WAVE_FORMAT_MULAW 0x0007 /* Microsoft Corporation */ #define WAVE_FORMAT_DTS 0x0008 /* Microsoft Corporation */ #define WAVE_FORMAT_DRM 0x0009 /* Microsoft Corporation */ #define WAVE_FORMAT_OKI_ADPCM 0x0010 /* OKI */ #define WAVE_FORMAT_DVI_ADPCM 0x0011 /* Intel Corporation */ #define WAVE_FORMAT_IMA_ADPCM (WAVE_FORMAT_DVI_ADPCM) /* Intel Corporation */ #define WAVE_FORMAT_MEDIASPACE_ADPCM 0x0012 /* Videologic */ #define WAVE_FORMAT_SIERRA_ADPCM 0x0013 /* Sierra Semiconductor Corp */ #define WAVE_FORMAT_G723_ADPCM 0x0014 /* Antex Electronics Corporation */ ... #define WAVE_FORMAT_MPEG 0x0050 /* Microsoft Corporation */ #define WAVE_FORMAT_RT24 0x0052 /* InSoft, Inc. */ #define WAVE_FORMAT_PAC 0x0053 /* InSoft, Inc. */ #define WAVE_FORMAT_MPEGLAYER3 0x0055 /* ISO/MPEG Layer3 Format Tag */ ... // // the WAVE_FORMAT_DEVELOPMENT format tag can be used during the // development phase of a new wave format. Before shipping, you MUST // acquire an official format tag from Microsoft. // #define WAVE_FORMAT_DEVELOPMENT (0xFFFF)
这是列表中的前几个编码器,“...”表示之间还有更多内容,我只是想展示 MP3 编码器标签,因为它现在通常是一种非常流行的格式。代码中的注释说明了您必须注册格式。
PCM 格式未列出,但其格式标签为 1。
Windows 音频架构
音频架构可以描述为多媒体架构和 Direct Sound 架构。本教程将仅涵盖 Windows 多媒体架构。有趣的是,如果找不到 Direct Sound 驱动程序,DSound 库实际上会回退到使用 Windows 多媒体 API。
下面是一个简单的文本图,展示了 Windows 多媒体层的工作原理(FYI,MSDN 上有更好的图)。
[ Your Windows Application ] | | [ Windows Multi-Media (WINMM.DLL) ] | | [ Audio Driver Media (WDMAUD.DRV)] USER MODE ----------------------------------------------------------- [ Audio Driver (WDMAUD.SYS) ] KERNEL MODE [ Kernel Mixer, Etc. ] [ Audio Hardware Miniport ]
这是一个基本架构。您的 Windows 应用程序将调用 waveOutOpen()
、waveOutWrite()
等,这些将通过 WINMM.DLL 进行过滤,最终到达 WDMAUD.DRV 的 wodMessage()
。WDMAUD.DRV 接着会将此请求排队,以便在您的进程空间中创建的单独线程中进行处理。然后,它将等待一个信号,表明它可以返回,并附带一个错误代码。
单独的线程将打开内核驱动程序,读/写数据到内核驱动程序,甚至向驱动程序发送 IOCTL。读/写是异步的,驱动程序使用 APC 回调(标准的 ReadFileEx
/WriteFileEx
)通知其完成。此回调会过滤回您的应用程序,并发出您指定的信号。您可以指定一个事件、一个窗口消息、一个回调函数或其他任何内容。我们稍后会讲到这些。
什么是 APC?APC 是异步过程调用 (Asynchronous Procedure Call)。如果线程处于可警报状态,那么下次调度时可以在该线程上执行一个函数。SendMessage()
就是一个 APC。APC 超出了本文的范围。有关 APC 的更多信息,请访问 MSDN 的以下 URL:MSDN。
内核中发生了什么?
我们不需要详细了解所有细节,但首先让我们谈谈混合。如果您使用的是 Windows 9x/ME,您会注意到一次只有一个应用程序可以打开和播放音频。如果您使用的是 Windows 2000、XP 或 2003,您会注意到多个应用程序可以同时打开音频设备并播放音频。这是为什么?这是因为这些操作系统支持内核混合。声音卡通常一次只能播放一个数据流。这是从简单的角度来看。也许有不同的音频硬件可以同时处理更多流,有些甚至支持混合样本流(Ala GUS)。但是,从我们的角度来看,让我们假设它支持一个音频流。这意味着多个应用程序不能同时使用该设备。所以,Windows 98 简单地允许播放一个流。Windows 2000 则不同,因为它有一个内核混合驱动程序。此混合器将执行所有音频流的收敛,并将它们混合成一个,然后再发送到音频硬件。我相信音频是在硬件中混合的,当可能时,但也有软件混合。我不知道同时可以打开多少音频流是否存在限制,也许没有,也许只要系统资源可用即可。
现在,音频需要通过硬件端口发送到声卡。如果软件必须逐字节地将数据发送到硬件端口,那将非常低效。这将导致 CPU 使用率升高,并耗尽 CPU 资源,因为此线程需要持续关注才能将音频发送到设备。答案是什么?DMA。直接内存访问 (Direct Memory Accessing)。
什么是 DMA 以及它是如何工作的?
DMA 允许数据在内存和设备之间移动。数据可以双向传输。其优点在于,这是独立于处理器进行的。这更有效率,因为 CPU 不需要处理将数据发送到设备或从设备读取数据。如何设置 DMA 传输?首先,您需要非分页内存。这种内存不能被分页到磁盘。这是因为 DMA 不与 CPU 交互,甚至不知道虚拟内存。如果内存被移到磁盘,DMA 将不知道。DMA 会导致系统崩溃吗?不会,它只是直接读取内存,并会尝试播放它。如果它正在填充内存而 CPU 正在使用它做其他事情,它可能会导致系统崩溃。DMA 不知道 CPU 如何组织内存。它只是获取一个地址并进行操作。这就是我们接下来关于 DMA 的规则。内存必须是连续的,并且您必须告诉 DMA 内存位置的物理地址。DMA 不知道虚拟内存地址,因此它需要物理地址。您需要做的最后一件事是编程 DMA 以与正确的设备交互,并告诉 DMA 以何种速度将数据发送到设备。设备也必须设置为接收数据。所有这些都通过编程 PIC(可编程中断控制器)来完成。
如何设置 DMA 和设备?
这超出了本文档的范围。我还没有在 Windows 中这样做过,也没有研究过如何做到。很可能,就像 Windows 中的所有其他东西一样,有一个 API 可以为您执行此操作。Windows 通常已经封装了 PC 的最常见组件,供您的驱动程序标准使用。我很久以前就在 DOS 中做到过。这可能超出了本教程的范围。但是,如果您想要源代码,我可以给您,我仍然拥有它。请注意,它是 8 年前为 Watcom 编写的。
Windows 应用程序
好的,回到正题,我们现在进入 Windows 的世界!这意味着,您不必担心设置 PIC、音频硬件、设置 ISR 或从只想播放声音的应用程序中进行 DMA 传输。您甚至不必知道如何转换音频编码器,您可能只需要知道如何读取文件格式。不过,即使是那个也可以在一个由他人编写的库中找到!
所以,在我们的应用程序中,我们只想打开一个 .WAV 文件并播放它。这首先需要我们了解 .WAV 文件格式。这应该很容易找到,互联网上有很多解释文件格式的资源。简单的谷歌搜索应该可以找到一个。
WAV 文件格式
波形文件格式被分解为 RIFF 块。某些波形文件是非标准的,包含额外的块和信息。我们编写的程序不关心这些,我们将只遵守传统的块。这是 .WAV 格式的一个简单分解。
RIFF
<4 bytes>
WAVE
这是您通常在格式中看到的第一件事,至少我是这样找到的。我曾尝试将这 4 个字节用作大小,但通常我发现最好跳过它们。所以,在我们编写的程序中,我只跳过前 12 个字节。接下来,所有内容都以 RIFF 的形式进行。RIFF 的工作方式如下:
RIFF IDENTIFIER (4 Bytes) RIFF SIZE (4 Bytes) <RIFF INFORMATION> (RIFF SIZE - 8)
所以,您有一个 4 字节的标识符,告诉您这是哪个块。接下来的 4 个字节告诉您此 RIFF 的大小。最后一个块是数据,它是 RIFF 大小减去头部的结果,即 RIFF SIZE - 8。WAVE 文件中的第一个 RIFF 将是“fmt ”。它描述如下:
"fmt " DWORD dwRiffSize; WORD wFormatTag; // Format category WORD wChannels; // Number of channels DWORD dwSamplesPerSec; // Sampling rate DWORD dwAvgBytesPerSec; // For buffer estimation WORD wBlockAlign; // Data block size WORD wBitsPerSample;
所以在程序中,我们只需跳过前 12 个字节,然后从文件中读取此信息。现在,我们唯一关心的其他 RIFF 是“data”。它的定义如下:
"data" 4 Bytes DATA SIZE 4 Bytes <YOUR Data Audio> (DATA SIZE - 8)
在我们的程序中,我们将简单地循环遍历 RIFF,忽略所有 RIFF 信息,直到找到数据块。然后,我们将数据块读入一个缓冲区,用作我们的音频。非常简单。
将其制作成库
我们将做的第一件事是将其制作成一个库。从面向对象的角度来看,我们将创建一个 Create
函数和一个 Destroy
函数。应用程序将能够简单地调用这些函数来获取对象的“句柄”。然后,该对象将用于调用库提供的函数。我们将支持另一个函数 Pause
,以暂停播放。我们制作的库将很简单,并且会无限循环播放声音。
让我们声明我们的导出函数。这是我们将使用的头文件:
/*********************************************************************** * testaudio.h * * Audio Library * * * Toby Opferman Copyright (c) 2003 * ***********************************************************************/ #ifndef __WAVELIB_H__ #define __WAVELIB_H__ typedef PVOID HWAVELIB; #ifdef __cplusplus extern "C" { #endif HWAVELIB WaveLib_Init(PCHAR pAudioFile, BOOL bPause); void WaveLib_UnInit(HWAVELIB hWaveLib); void WaveLib_Pause(HWAVELIB hWaveLib, BOOL bPause); #ifdef __cplusplus } #endif #endif
该库将简单地返回波形库的句柄。调用应用程序不需要知道实现是什么,也不需要知道 HWAVELIB
句柄是什么。该库可以只返回一个数字、一个指针,或者几乎任何东西,只要波形库本身可以将其转换为可用于检索该句柄的会话信息的内容。在我们的例子中,我们将简单地返回指针。如果应用程序不知道数据是什么,它就无法对其进行操作。用户*可以*进行逆向工程并找出它是什么,但话说回来,这又不是国家安全,而且您可以对任何东西都这样做,甚至是 C++ 对象。我们只是在播放 WAV 文件!
应用程序使用它只需执行以下操作:
HWAVELIB hWaveLib = NULL; hWaveLib = WaveLib_Init(pszFileName, FALSE); if(hWaveLib) { printf(" Press a key to stop> "); getch(); WaveLib_UnInit(hWaveLib); }
源代码格式
您通常可以使用任何您想要的样式,但是,我想解释一下我的样式,因为您将阅读我的源代码。我喜欢以 <ModuleName>_<Function>
的形式编写函数。这使得代码易于阅读,尤其是在大型项目中,因为您可以轻松知道函数的位置。我看到模块名称,就可以直接找到源代码。如果我在调试,这些名称会显示在堆栈跟踪中。当然,使用 PDB、源代码等信息也可能会显示出来,但快速查看函数堆栈可以轻松找到位置。我只是觉得这样更方便,您有权采用自己的方法。
打开 WAV 设备
要打开波形设备,我们只需填写 WAVE FORMAT 结构,决定回调机制,然后调用 waveOutOpen()
。
if(waveOutOpen(&pWaveLib->hWaveOut, // Return Wave Handle Location WAVE_MAPPER, // Wave Device #, WAVE_MAPPER means default &pWaveLib->WaveSample.WaveFormatEx, // The Wave Format Structure (ULONG)WaveLib_WaveOutputCallback, // Callback Mechanism, a Function callback (ULONG)pWaveLib, // User Data To Send to Callback CALLBACK_FUNCTION // Callback/Notification type (Function) ) != MMSYSERR_NOERROR) { WaveLib_UnInit((HWAVELIB)pWaveLib); pWaveLib = NULL; }
如前所述,您可以使用其他类型的通知,而不是回调。Microsoft 头文件在 MMSYSTEM.H 中定义了这些。
/* flags used with waveOutOpen(), waveInOpen(), midiInOpen(), and */ /* midiOutOpen() to specify the type of the dwCallback parameter. */ #define CALLBACK_TYPEMASK 0x00070000l /* callback type mask */ #define CALLBACK_NULL 0x00000000l /* no callback */ #define CALLBACK_WINDOW 0x00010000l /* dwCallback is a HWND */ #define CALLBACK_TASK 0x00020000l /* dwCallback is a HTASK */ #define CALLBACK_FUNCTION 0x00030000l /* dwCallback is a FARPROC */ #ifdef _WIN32 #define CALLBACK_THREAD (CALLBACK_TASK)/* thread ID replaces 16 bit task */ #define CALLBACK_EVENT 0x00050000l /* dwCallback is an EVENT Handle */ #endif
设备应该可以打开,只要它支持该格式(并且设备尚未被打开,正如 Windows 9x/ME 那样)。回调定义如下:
/*********************************************************************** * WaveLib_WaveOutputCallback * * Audio Callback * * Parameters * * * Return Value * Handle To This Audio Session * ***********************************************************************/ void CALLBACK WaveLib_WaveOutputCallback(HWAVEOUT hwo, // Wave Handle UINT uMsg, // Message DWORD dwInstance, // User Defined Data (Specified in waveOutOpen) DWORD dwParam1, // Message Data 1 DWORD dwParam2) // Message Data 2 { PWAVELIB pWaveLib = (PWAVELIB)dwInstance; switch(uMsg) { case WOM_OPEN: // Wave device is being opened WaveLib_WaveOpen(hwo, pWaveLib); break; case WOM_DONE: // A wave buffer is finished playing WaveLib_WaveDone(hwo, pWaveLib); break; case WOM_CLOSE: // Wave device is being closed WaveLib_WaveClose(hwo, pWaveLib); break; } }
接下来的任务是播放实际的声音!
播放声音
音频的问题在于它是连续的。这意味着我们永远不希望音频驱动程序没有缓冲区,否则您会听到跳帧或静音!所以,我们所做的是将声音分成固定长度的缓冲区。方法可以是每个缓冲区持续 1/5 秒,将声音分成 5 个缓冲区。音频回调会发生,我们会重新使用缓冲区。
在我编写的应用程序中,我只是硬编码了 8 个缓冲区,每个缓冲区 8000 字节。在一个更健壮的应用程序中,您通常应该根据正在播放的音频分配这些缓冲区和大小。例如,我会计算播放一定时间(例如 1 或 2 秒)所需的字节数,然后将其分成 5 个或更多缓冲区。您应该进行实验以确定正确的值,以防止跳帧!
所以,为了播放音频缓冲区,我们只需要执行以下操作:
typedef struct wavehdr_tag { LPSTR lpData; /* pointer to locked data buffer */ DWORD dwBufferLength; /* length of data buffer */ DWORD dwBytesRecorded; /* used for input only */ DWORD_PTR dwUser; /* for client's use */ DWORD dwFlags; /* assorted flags (see defines) */ DWORD dwLoops; /* loop control counter */ struct wavehdr_tag FAR *lpNext; /* reserved for driver */ DWORD_PTR reserved; /* reserved for driver */ } WAVEHDR, *PWAVEHDR, NEAR *NPWAVEHDR, FAR *LPWAVEHDR;
您需要用数据缓冲区和音频大小设置此结构。然后,您只需调用 waveOutPrepareHeader
来初始化此结构。然后,您可以调用 waveOutWrite()
来播放数据。然后,您只需等待回调,找到已播放完毕的缓冲区,用下一个样本重新填充它,然后再次调用 waveOutWrite()
!在我编写的程序中,我只是使用了一个事件来信号我的主线程,以便它循环遍历缓冲区。这是帮助防止任何死锁问题(本文后面“请注意”部分有描述)的简单方法。这是我使用的代码。WaveLib_AudioBuffer
是我用来帮助解析音频缓冲区并连续地将 .WAV 样本循环输入音频设备的函数。
for(Index = 0; Index < 8; Index++) { pWaveLib->WaveHdr[Index].dwBufferLength = SAMPLE_SIZE; pWaveLib->WaveHdr[Index].lpData = pWaveLib->AudioBuffer[Index]; waveOutPrepareHeader(pWaveLib->hWaveOut, &pWaveLib->WaveHdr[Index], sizeof(WAVEHDR)); WaveLib_AudioBuffer(pWaveLib, Index); waveOutWrite(pWaveLib->hWaveOut, &pWaveLib->WaveHdr[Index], sizeof(WAVEHDR)); }
最后一个组件是循环,它简单地在缓冲区被释放后继续将音频数据馈送到设备。它等待一个当收到 WOM_DONE
通知时会触发的事件。循环简单地遍历所有缓冲区,读取标志。如果标志中设置了 DONE
位,我们就移除它,用声音重新填充它,然后播放!
while(!pWaveLib->bWaveShouldDie) { WaitForSingleObject(pWaveLib->hEvent, INFINITE); for(Index = 0; Index < 8; Index++) { if(pWaveLib->WaveHdr[Index].dwFlags & WHDR_DONE) { WaveLib_AudioBuffer(pWaveLib, Index); waveOutWrite(pWaveLib->hWaveOut, &pWaveLib->WaveHdr[Index], sizeof(WAVEHDR)); } } }
清理代码只需将 bWaveShouldDie
设置为 TRUE
,然后调用对象上的 SetEvent
,然后简单地等待线程句柄,直到它被触发才能退出。bWaveShouldDie
需要先设置,否则我们的线程可能会错过调度,并且线程可能不会死亡。当然,只有在音频暂停的情况下才会发生这种情况,因为如果我们通过 WOM_DONE
通知播放音频,我们仍然应该收到事件信号。
void WaveLib_UnInit(HWAVELIB hWaveLib) { PWAVELIB pWaveLib = (PWAVELIB)hWaveLib; if(pWaveLib) { if(pWaveLib->hThread) { pWaveLib->bWaveShouldDie = TRUE; SetEvent(pWaveLib->hEvent); WaitForSingleObject(pWaveLib->hThread, INFINITE); CloseHandle(pWaveLib->hEvent); CloseHandle(pWaveLib->hThread); } if(pWaveLib->hWaveOut) { waveOutClose(pWaveLib->hWaveOut); } if(pWaveLib->WaveSample.pSampleData) { LocalFree(pWaveLib->WaveSample.pSampleData); } LocalFree(pWaveLib); } }
关闭设备
最后需要注意的是,您必须在完成后关闭设备句柄。停止向声音驱动程序提供缓冲区,然后调用 WaveOutReset()
。此 API 将简单地取消所有挂起的缓冲区并将它们返回给应用程序。如果驱动程序中有挂起的缓冲区,WaveOutClose
将失败!因此,请记住停止提供缓冲区,调用 waveOutReset
,然后调用 waveOutClose()
。上面的代码在另一个线程中调用 reset,这就是为什么 uninit
中没有它。这是一个 FYI,因为它并不明显,而且我也没有详细说明。通常,waveOutReset()
会在所有缓冲区返回之前不返回,但我见过一些代码,在调用 waveOutReset()
后会等待缓冲区设置 DONE 位。您可以按照自己的方式进行,但根据我的系统经验,您只需调用 API,确保不再调用 waveOutWrite()
,然后调用 waveOutClose()
。
请注意
您还记得我告诉您音频驱动程序会在您的进程中创建一个单独的线程来执行操作吗?它们使用临界区和保护来保护线程消息队列。如果您尝试在线程回调例程中执行某些调用,可能会导致死锁。一个例子是如果您尝试循环数据。假设您使用 waveInOpen()
打开波形输入设备。然后,您打开输出设备。然后,您尝试在 WaveIn
回调中通过 waveOut
设备发送数据。这种情况可能导致死锁。原因是,特别是当您创建一个试图播放数据的线程时,它正在发送 waveOutWrite()
,而该数据需要在回调发布的波形线程上进行处理。现在该线程正在等待波形线程执行写入。然而,回调函数现在试图执行一个也需要获取临界区的操作。这就导致了死锁!要小心这一点……
PlaySound()
所以,整篇文章可以用一行代码来概括。将文件名传递给此 API,它就会为您播放声音。但是,那样有什么乐趣呢,您又会学到什么呢?本教程可能没有教您如何播放 MP3。但是,它是您使用相同架构播放任何内容的基石,只需加入文件解析和音频流转换即可。此外,难道这甚至还不够有趣吗?
结论
音频不像以前那样有趣了,那时您必须自己处理硬件,但现在应用程序使用音频更容易了。我希望您至少从阅读本文中学到了一些东西!
附注:我知道在重播循环中,某些音频文件会出现跳帧。看看您是否能修复它!:)