摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P989
波形音频(waveform audio)是 Windows 最常用的多媒体功能。波形音频设备能够通过麦克风捕捉声音,将其转换成数字,存放在内存中或以.WAV 扩展名的波形文件的形式存储在磁盘上。随后,这些声音可以被重新播放出来。
22.2.1 声音和波形
在深入研究波形音频 API 之前,有必要先了解一下声音的物理性状和人类对声音的感知,以及计算机输入/输出声音的过程。
声音是一种振动。人体能感知声音,是因为它改变了作用在我们的耳膜上的空气压力。麦克风可以接收到这些振动,并将其转化为电流。同样,电流可以被发送到放大器或扬声器,转换回声音。在传统的模拟形式的声音存储媒介(如录音带和唱片)中,这些振动以磁脉冲或波形槽形式存储。当声音被转化为电流时,它可以由一个振动波形来表示,波形显示了振动随着时间推移的变化情况。最自然的振动形式以正弦波为代表,本书先前的图 5-7 展示了正弦波的一个周期。
正弦波有两个参数,振幅(即一个周期过程中振动的最大幅度)和频率。对我们来说,振幅就是声音强度,频率就是音调。人类的耳朵总体来说对 20Hz(即每秒有几个周期)的低音调到 20000Hz 的高音调这个区间范围内的正弦波比较敏感,不过对较高的声音的敏感性会随着年龄的增长而降低。
人类对频率的感知是和频率的对数成正比的,而不是直接线性相关的。这就是说,我们认为频率从 20Hz 变化到 40Hz 与频率从 40Hz 变化到 80Hz 是相同的。在音乐中,这种翻倍的频率定义为八度。因此,人类耳朵对约 10 个八度的声音比较敏感。钢琴的音域范围是略超过 7 个八度,从 27.5Hz 到 4186Hz。
虽然正弦波代表最自然的振动,但是在自然界中很少存在完全单纯的正弦波。而且,单纯的正弦波并不是很好听。大多数声音都要复杂得多。
任何周期性的波形(即波形不断地重复自身)都可以分解成多个正弦波,它们的频率的关系是整数倍数。这就是所谓的傅里叶级数,这是以法国数学家和物理学家让 ● 巴蒂斯特 ● 约瑟夫 ● 傅里叶(Jean Baptiste Joseph Fourier,1768~1830)命名的。周期的频率称为基波。该系列中的其他正弦波的频率分别是基波频率的 2、3、4……倍。这些都是所谓的泛音。基波也称作一次谐波。一次泛音则是二次谐波,以此类推。
正弦波谐波的相对强度给了每个周期波形一个独特的声音。这就是所谓的“音色”,就是这个让小号听起来像小号,钢琴听起来像钢琴。
各种乐器的谐波的相对强度不同,且谐波随着时间推移、演奏的音符不同而变化。特别是,使用乐器开始演奏音符时(即音符的起音),情况可能十分复杂,而这对我们对音色的感知是非常重要的。
由于近年来数字存储能力的不断增长,使得直接以数字存储声音而无需复杂的结构称为可能。
22.2.2 脉冲编码调制
计算机以数字形式工作,所以为了把声音输入我们的计算机,有必要制定一种能将声音转化为数字,再把数字转化回声音的机制。
脉冲编码调制从概念上来说是一种十分简单的过程,但这个复杂的名字听上去比较好听。
脉冲编码调制以固定的周期频率对波形进行采样,通常每秒几万次。对于每个样品,需要测量波形的幅值。模数转换器(analog-to-digital converter,ADC)负责将振幅转换为数字。同样,数模转换器(digital-to-analog,DAC)可将数字转换回电子波形。播放出来的声音和之前输入的声音并不完全相同,因为这样产生的波形在高频部分具有尖锐的边缘。处于这个原因,播放的硬件往往在数模转换器之后再连接一个低通滤波器。该滤波器能剔除高频率,产生平滑的波形。在输入端,低通滤波器被放置在 ADC 之前。
脉冲编码调制有两个参数:采样率(即每秒测量波形幅值的次数)和采样大小(即存储幅值水平的位数)。正如你可能已经猜到的那样,采样率越高、采样大小越大,就能越好地还原原始声音。然而,到某个时刻,任何对采样率和采样大小的改进都无法起作用了,因为已经超越了人类感知的分辨范围。另一方面,采样率和采样大小过低,可能会在还原音乐或其他声音的准确性方面产生问题。
22.2.3 采样率
特别要指出的是,采样率必须两倍于采样声音的最高频率。这被称为“奈奎斯特频率”,是以 20 世纪 30 年代研究采样过程的工程师 Harry Nyquist 的名字命名的。
如果使用过低的采样率对正弦波进行采样,产生的波形的频率会比原始版本低。这就是所谓的走样。为了避免这个走样问题,应在输入端使用低通滤波器来阻止所有大于采样率一半的频率。在输出端,数模转换器所产生的波形的粗糙边缘实际上是大于采样率一半的频率组成的谐波。因此,输出端的低通滤波器也会阻止输出所有超过采样率一半的频率。
音频 CD 使用的采样率是每秒 44100 个样品,即 44.1kHz。这以特定数字的由来如下。
为了应付我们可能同时录制数字音频与视频的情况,采样率应是美国和欧洲电视帧速率(分别是30Hz 和 25Hz)的整数倍。这样就把采样率推高到了 44.1kHz。
CD 的采样率为 44.1kHz,这会产生大量的数据,对一些应用程序来说可能过多了,比如录制语音而不是音乐的时候。如果减半采样率到 22.05kHz,则使得可还原声音的范围降低了一个八度,也就是 10kHz。再次减半到 11.025kHz,则使我们的频率降低到 5kHz。44.1kHz,22.05kHz,11.025kHz 以及 8kHz 的采样率,是波形音频设备支持的常用标准。
你可能会认为,11.025kHz 的采样率足够录制钢琴的声音,因为钢琴的最高频率是 4186Hz。然而,4186Hz 只是钢琴最高的基波频率。如果去除所有 5000Hz 以上的正弦波就会减少可被还原的谐波,那样就无法准确地捕捉和还原钢琴的声音了。
22.2.4 采样大小
这就是所谓的动态范围。
声音强度是波形振幅(即每个正弦波在一个周期过程中可达到最大幅度的合成)的平方。和频率相同,人类对声音强度的感觉是和波形振幅的对数成正比的。
增加 1 分贝表示声音强度增加 1.26 倍(即 10 的 10 次方根),或波形振幅增加 1.12(10 的 20 次方根)。1 分贝大约是人耳可以分辨的最小声音强度增加量。从人耳刚刚可辨别的声音强度阈值,到令人感到疼痛的声音强度阈值,它们之间的差别大约是 100 分贝。
你可以用以下公式计算两个声音之间的动态范围,单位为分贝:
dB = 20 * log(A1 / A2)
其中 A1 和 A2 是两个声音的振幅。采样大小为 1 位时,动态范围为 0,因为只可能有一个振幅。
采样大小为 8 位时,最大振幅是最小振幅的 256 倍。因此,动态范围是 48 分贝,计算公式如下:
dB = 20 * log(256)
48 分贝的动态范围大约是一个安静房间和一台运行着电动割草机之间的区别。如果将采样大小增加一倍到 16 位,产生的动态范围则为 96 分贝,计算公式如下:
dB = 20 * log(65536)
这非常接近听力最低阈值和产生痛感之间的区别,它被认为非常适合还原音乐。
Windows 支持 8 位和 16 位两种采样大小。当储存 8 位样本时,样本被视为一个无符号的字节。所以无声状态将被存储为一个值为 0x80 的串。16 位样被被视为一个有符号的整数,所以无声状态将被保存为一个由 0 组成的串。
如果要计算存储未压缩音频所需占用的空间,只要将声音长度的秒数乘以采样率即可。如果使用的是 16 位的样本而不是 8 位的样本的话,再讲这个数字乘以 2。如果你录的是立体声,则将此数字再乘以 2。例如,1 小时的 CD 音质声音(即长度为 3600 秒,采样率为每秒 44100 个样本,因为是立体声,所以采样大小为每个样本 2 个字节)需要占用 635 MB,这个值非常接近 CD-ROM 的存储上限,当然这并不是一种简单的巧合。
22.2.5 用软件生产正弦波
我们关于波形音频的第一个练习,不是把声音保存到文件或播放录制的声音。我们将使用底层波形音频 API(即以 waveOut 前缀开头的函数)来创建一个名为 SINEWAVE 的音频正弦波生成器。这个程序能生成从 20Hz(人类感知的最低值)到 5000Hz(比人类感知的最高值低两个八度音阶),以 1Hz 递增的正弦波。
只需将代表波形(在本例中就是正弦波)的数据填写到缓冲区,并将其传给 API。(实际上比这个要复杂一些,稍后我会提供更多细节。)当波形音频硬件播放完缓冲区内的内容后,你再将第二个缓冲区传给它,以此类推。
在第一次考虑这个问题(且不知道什么是 PCM)时,可能会认为将一个周期的正弦波分割为固定数量的样本是十分合理的(例如分割成 360 份)。对于 20Hz 的正弦波,这样每秒可以输出 7200 个样本。对于 200Hz 的正弦波,每秒会输出 72000 个样本。这可能可行,但并不是正确的做法。对于 5000Hz 的正弦波,这会需要每秒输出 1800 000 个样本,这必将使 DAC 承受很重的负担!此外,对于更高的频率,这种做法比所需要的精度高出了太多。
一般来说,每个周期的样本数量等于样本采样率除以所设计的正弦波的频率。一旦你知道了每个周期的样本数量,就可以用这个数字去除 2π 弧度,再使用 sin 函数来获取一个周期的样本。然后,只需不断重复这个周期的样本,就可以产生连续的波形。
问题是,每个周期的样本数量很可能是小数,这样得到的波形在每个周期的末尾都不连续,所以这个方法也不行。
使这个方法正确工作的关键是需要维持一个静态的“相位角” (phase angle)变量。这个角度初始值为 0.第一个样本是 0 度的正弦值。然后相位角递增,递增的值为频率的 2π 倍 除以采样率。此时使用该相位角产生第二个样本,并以这种方式继续下去。每当相位角超过 2π 弧度时,就从中减去 2π 弧度。但千万不要把相位角重新初始化为 0。
当为后续的缓冲区创建数据时,应继续递增最近的相位角的值,记住不要将其重新初始化为 0。
图 22-2 是 SINEWAVE 程序,其中的 FillBuffer 函数完成了上述任务。
/*--------------------------------------------------------
SINEWAVE.C -- Multimedia Windows Sine Wave Generator
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include <math.h>
#include "resource.h"
#define SAMPLE_RATE 11025
#define FREQ_MIN 20
#define FREQ_MAX 5000
#define FREQ_INIT 440
#define OUT_BUFFER_SIZE 4096
#define PI 3.14159
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("SineWave");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
if (-1 == DialogBox(hInstance, szAppName, NULL, DlgProc))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
}
return 0;
}
VOID FillBuffer(PBYTE pBuffer, int iFreq)
{
static double fAngle;
int i;
for (i = 0; i < OUT_BUFFER_SIZE; i++)
{
pBuffer[i] = (BYTE)(127 + 127 * sin(fAngle));
fAngle += 2 * PI * iFreq / SAMPLE_RATE;
if (fAngle > 2 * PI)
fAngle -= 2 * PI;
}
}
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bShutOff, bClosing;
static HWAVEOUT hWaveOut;
static HWND hwndScroll;
static int iFreq = FREQ_INIT;
static PBYTE pBuffer1, pBuffer2;
static PWAVEHDR pWaveHdr1, pWaveHdr2;
static WAVEFORMATEX waveformat;
int iDummy;
switch (message)
{
case WM_INITDIALOG:
hwndScroll = GetDlgItem(hwnd, IDC_SCROLL);
SetScrollRange(hwndScroll, SB_CTL, FREQ_MIN, FREQ_MAX, FALSE);
SetScrollPos(hwndScroll, SB_CTL, FREQ_INIT, TRUE);
SetDlgItemInt(hwnd, IDC_TEXT, FREQ_INIT, FALSE);
return TRUE;
case WM_HSCROLL:
switch (LOWORD(wParam))
{
case SB_LINELEFT: iFreq -= 1; break;
case SB_LINERIGHT: iFreq += 1; break;
case SB_PAGELEFT: iFreq /= 2; break;
case SB_PAGERIGHT: iFreq *= 2; break;
case SB_THUMBTRACK:
iFreq = HIWORD(wParam);
break;
case SB_TOP:
GetScrollRange(hwndScroll, SB_CTL, &iFreq, &iDummy);
break;
case SB_BOTTOM:
GetScrollRange(hwndScroll, SB_CTL, &iDummy, &iFreq);
break;
}
iFreq = max(FREQ_MIN, min(FREQ_MAX, iFreq));
SetScrollPos(hwndScroll, SB_CTL, iFreq, TRUE);
SetDlgItemInt(hwnd, IDC_TEXT, iFreq, FALSE);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_ONOFF:
// If turning on waveform, hWaveOut is NULL
if (hWaveOut == NULL)
{
// Allocate memory for 2 headers and 2 buffers
pWaveHdr1 = (PWAVEHDR)malloc(sizeof(WAVEHDR));
pWaveHdr2 = (PWAVEHDR)malloc(sizeof(WAVEHDR));
pBuffer1 = (PBYTE)malloc(OUT_BUFFER_SIZE);
pBuffer2 = (PBYTE)malloc(OUT_BUFFER_SIZE);
if (!pWaveHdr1 || !pWaveHdr2 || !pBuffer1 || !pBuffer2)
{
if (pWaveHdr1) free(pWaveHdr1);
if (pWaveHdr2) free(pWaveHdr2);
if (pBuffer1) free(pBuffer1);
if (pBuffer2) free(pBuffer2);
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, TEXT("Error allocating memory!"),
szAppName, MB_ICONEXCLAMATION | MB_OK);
return TRUE;
}
// Variable to indicate off button pressed
bShutOff = FALSE;
// Open waveform audio for output
waveformat.wFormatTag = WAVE_FORMAT_PCM;
waveformat.nChannels = 1;
waveformat.nSamplesPerSec = SAMPLE_RATE;
waveformat.nAvgBytesPerSec = SAMPLE_RATE;
waveformat.nBlockAlign = 1;
waveformat.wBitsPerSample = 8;
waveformat.cbSize = 0;
if (waveOutOpen(&hWaveOut, WAVE_MAPPER, &waveformat,
(DWORD)hwnd, 0, CALLBACK_WINDOW) != MMSYSERR_NOERROR)
{
free(pWaveHdr1);
free(pWaveHdr2);
free(pBuffer1);
free(pBuffer2);
hWaveOut = NULL;
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, TEXT("Error opening waveform audio device!"),
szAppName, MB_ICONEXCLAMATION | MB_OK);
return TRUE;
}
// Set up headers and prepare them
pWaveHdr1->lpData = (LPSTR)pBuffer1;
pWaveHdr1->dwBufferLength = OUT_BUFFER_SIZE;
pWaveHdr1->dwBytesRecorded = 0;
pWaveHdr1->dwUser = 0;
pWaveHdr1->dwFlags = 0;
pWaveHdr1->dwLoops = 1;
pWaveHdr1->lpNext = NULL;
pWaveHdr1->reserved = 0;
waveOutPrepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
pWaveHdr2->lpData = (LPSTR)pBuffer2;
pWaveHdr2->dwBufferLength = OUT_BUFFER_SIZE;
pWaveHdr2->dwBytesRecorded = 0;
pWaveHdr2->dwUser = 0;
pWaveHdr2->dwFlags = 0;
pWaveHdr2->dwLoops = 1;
pWaveHdr2->lpNext = NULL;
pWaveHdr2->reserved = 0;
waveOutPrepareHeader(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));
}
// If turning off waveform, reset waveform audio
else {
bShutOff = TRUE;
waveOutReset(hWaveOut);
}
return TRUE;
}
break;
// Message generated from waveOutOpen call
case MM_WOM_OPEN:
SetDlgItemText(hwnd, IDC_ONOFF, TEXT("Turn Off"));
// Send two buffers to waveform output device
FillBuffer(pBuffer1, iFreq);
waveOutWrite(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
FillBuffer(pBuffer2, iFreq);
waveOutWrite(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));
return TRUE;
// Message generated when a buffer is finished
case MM_WOM_DONE:
if (bShutOff)
{
waveOutClose(hWaveOut);
return TRUE;
}
// Fill and send out a new buffer
FillBuffer((PBYTE)((PWAVEHDR)lParam)->lpData, iFreq);
waveOutWrite(hWaveOut, (PWAVEHDR)lParam, sizeof(WAVEHDR));
return TRUE;
case MM_WOM_CLOSE:
waveOutUnprepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
waveOutUnprepareHeader(hWaveOut, pWaveHdr2, sizeof(WAVEHDR));
free(pWaveHdr1);
free(pWaveHdr2);
free(pBuffer1);
free(pBuffer2);
hWaveOut = NULL;
SetDlgItemText(hwnd, IDC_ONOFF, TEXT("Turn On"));
if (bClosing)
EndDialog(hwnd, 0);
return TRUE;
case WM_SYSCOMMAND:
switch (wParam)
{
case SC_CLOSE:
if (hWaveOut != NULL)
{
bShutOff = TRUE;
bClosing = TRUE;
waveOutReset(hWaveOut);
}
else
EndDialog(hwnd, 0);
return TRUE;
}
break;
}
return FALSE;
}
SINEWAVE.RC (excerpts)
// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"
/
//
// Dialog
//
SINEWAVE DIALOG DISCARDABLE 100, 100, 200, 50
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Sine Wave Generator"
FONT 8, "MS Sans Serif"
BEGIN
SCROLLBAR IDC_SCROLL, 8, 8, 150, 12
RTEXT "440", IDC_TEXT, 160, 10, 20, 8
LTEXT "Hz", IDC_STATIC, 182, 10, 12, 8
PUSHBUTTON "Turn On", IDC_ONOFF, 80, 28, 40, 14
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ generated include file.
// Used by SineWave.rc
#define IDC_STATIC -1
#define IDC_SCROLL 1000
#define IDC_TEXT 1001
#define IDC_ONOFF 1002
注意,FillBuffer 函数中使用的 OUT_BUFFER_SIZE、SAMPLE_RATE 和 PI 标识符定义在程序的开头。FillBuffer 的 iFreq 参数表示所需的频率,以 Hz 为单位。请注意,sin 函数的结果被扩展到介于 0~254 之间。对于每个样本,sin 函数的 fAngle 参数持续增加,增加值为 2π 弧度乘以所需的频率再除以采样率(即相位角)。
SINEWAVE 的窗口包含三个控件:一个水平滚动条用于选择频率,一个静态文本控件表明当前选定的频率,以及一个 Turn On 按钮。当单机该按钮时,就可以ongoing连续的声卡的扬声器上听到正弦波的声音,而按钮上的文本将变为“Turn Off”。你可以用键盘或鼠标拉动滚动条来改变频率。再次单机按钮就可以关闭声音。
SINEWAVE 的代码在 WM_INITDIALOG 消息中初始化滚动条,使得最低频率为 20Hz,最高频率为 5000Hz。滚动条的初始值把评论设置为 440Hz。用音乐的词汇来说,这就是中音 C 调的 A 音,是用来校正乐队的音符。DlgProc 收到 WM_HSCROLL 消息时,会改变静态变量 iFreq。请注意。向左翻页和向右翻页将导致 DlgProc 减少或增加一个八度音的频率。
当 DlgProc 收到按钮传来的 WM_COMMAND 消息时,它先分配 4 块内存——2 块给 WAVEHDR 结构(稍后会讨论),2 块用作缓冲区,分别名为 pBuffer1 和 pBuffer2,这两个缓冲区用于保存波形数据。
SINEWAVE 调用 waveOutOpen 函数打开波形音频设备用于输出,该函数使用如下参数:
waveOutOpen(&hWaveOut, wDeviceId, &waveformat, dwCallBack,
dwCallbackData, dwFlags);
第一个参数设置为指向一个 HWAVEOUT(handle to waveform audio output)类型的变量。在函数返回时,此变量的值就是在随后的波形输出调用中使用的句柄。
如果当首选设备无法满足需求,而其他某个设备可用时,系统会自动选择另一个设备。
第四个参数是一个窗口句柄,或是一个指向动态链接库中的回调函数的指针。这个参数指明了接收波形输出消息的窗口或回调函数。如果你使用回调函数,则可以在第五个参数中指定一些额外定义的数据。dwFlags 参数可被设为 CALLBACK_WINDOW 或 CALLBACK_FUNCTION 以指明第四个参数是什么类型。你也可以使用标志 WAVE_FORMAT_QUERY 来检查设备是否可以被打开,而不用实际去打开它。还有一些其他标志也可以用在这里。
waveOutOpen 的第三个参数被定义为指向 WAVEFORMATEX 类型的结构的指针,该结构在 MMSYSTEM.H 中的定义如下:
typedef struct waveformat_tag
{
WORD wFormatTag; // waveform format = WAVE_FORMAT_PCM
WORD nChannels; // number of channels = 1 or 2
DWORD nSamplesPerSec; // sample rate
DWORD nAvgBytesPerSec; // bytes per second
WORD nBlockAlign; // block alignment
WORD wBitsPerSample; // bits per samples = 8 or 16
WORD cbSize; // 0 for PCM
} WAVEFORMATEX, * PWAVEFORMATEX;
采样率(nSamplesPerSec)、采样大小(wBitsPerSample)以及你需要单声道还是立体声(nChannels)。这个结构中的某些信息可能看起来是重复的,这是因为该结构并不是完全为 PCM 设计的,还可用于其他采样方法。如果使用的不是 PCM,需要将最后一个字段设置为非零值,其他信息照旧。
如果使用的是 PCM,则需要把 nBlockAlign 字段设置为 nChannels 和 wBitsPerSample 的乘积再除以 8。这是每个样本占用的总的字节数。再把 nAvgBytesPerSec 字段设置为 nSamplesPerSec 和 nBlockAlign 的乘积。
SINEWAVE 初始化 WAVEFORMATEX 结构内的字段并使用如下方法调用 waveOutOpen 函数:
waveOutOpen(&hWaveOut, WAVE_MAPPER, &waveformat,
(DWORD)hwnd, 0, CALLBACK_WINDOW)
如果函数执行成功,waveOutOpen 函数返回 MMSYSERR_NOERROR(定义为 0);否则返回一个非零的错误代码。如果 waveOutOpen 返回非零值,SINEWAVE 会做一些清理工作,并显示一个消息框表示错误。
现在,设备以及打开了,SINEWAVE 继续初始化两个 WAVEHDR 结构的字段,这两个结构用于通过 API 向缓冲区传送数据。WAVEHDR 定义如下:
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;
SINEWAVE 将 lpData 字段设置为存储数据的缓冲区的地址,dwBufferLength 为此缓冲区的大小,dwLoops 为 1。所有其他字段都可以设为 0 或者 NULL。如果你想播放反复循环的声音,可以通过设定 dwFlags 和 dwLoops 字段来做到。
接下来,SINEWAVE 调用了 waveOutPrepareHeader 来准备这两个 WAVEHDR 结构。调用此函数可以避免 WAVEHDR 结构和缓冲区从内存被交换到磁盘去。
waveOutOpen 函数会向程序的消息队列发送一条MM_WOM_OPEN 消息。该消息的wParam 参数被设为波形输出句柄。SINEWAVE 处理 MM_WOM_OPEN 消息,两次调用 FillBuffer 向 pBuffer 缓冲区填充正弦波数据。然后 SINEWAVE 将两个 WAVEHDR 结构传给waveOutWrite。这个函数通过向波形输出硬件传输数据,真正开始播放声音。
MM_WOM_DONE 消息。它的 wParam 参数是波形输出句柄,lParam 是一个指向 WAVEHDR 结构的指针。SINEWAVE 处理该消息,计算新的缓冲区的值,并调用 waveOutWrite 重新提交缓冲区。
SINEWAVE 使用的“双缓冲”技术可以防止声音间的停顿。
当用户单击 Trun Off 按钮来关闭声音时,DlgProc 会接收到另一条 WM_COMMAND 消息。DlgProc 收到此消息后,把 bShutOff 变量设置为 TRUE,并调用 waveOutReset 函数。waveOutReset 函数会停止声音处理,并生成一条 MM_WOM_DONE 消息。当 bShutOff 值为 TRUE 时,SINEWAVE 通过调用 waveOutClose 来处理 MM_WOM_DONE 消息。这反过来又生成了一条 MM_WOM_CLOSE 消息。对 MM_WOM_CLOSE 的处理主要涉及清理工作。SINEWAVE 为两个 WAVEHDR 结构分别调用 waveOutUnprepareHeader 函数,释放所有内存块,并将按钮的文本设回“Turn On”。
如果波形硬件仍然在播放缓冲区内容,则调用 waveOutClose 函数本身没有任何效果。必须首先调用 waveOutReset 来停止播放并生成一条 MM_WOM_DONE 消息。当 wParam 值为 SC_CLOSE 时,DlgProc 还会处理 WM_SYSCOMMAND 消息。这个消息是当用户从系统菜单中选择“Close”选项时产生的。如果波形音频仍在播放,DlgProc 会调用 waveOutReset 函数。无论如何,程序最终会调用 EndDialog 函数以关闭该对话框并结束程序。