22.2.6 数字录音机
Windows 包含了一个叫做 Sound Recorder(录音机)的程序,它可以让你用数字形式录制和播放声音。图 22-3 中所示的程序(RECORD1)不像【录音机】程序那么复杂,因为它没有任何的文件 I/O,也不支持声音的编辑。但是,它的确展示了如何用底层的波形音频 API 来录制和回放声音。
/*--------------------------------------------------------
RECORD1.C -- Waveform Audio Recorder
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include "resource.h"
#define INP_BUFFER_SIZE 16384
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("Record1");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
if (-1 == DialogBox (hInstance, TEXT ("Record"), NULL, DlgProc))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
}
return 0 ;
}
void ReverseMemory(BYTE* pBuffer, int iLength)
{
BYTE b;
int i;
for (i = 0; i < iLength / 2; i++)
{
b = pBuffer[i];
pBuffer[i] = pBuffer[iLength - i - 1];
pBuffer[iLength - i - 1] = b;
}
}
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bRecording, bPlaying, bReverse, bPaused,
bEnding, bTerminating;
static DWORD dwDataLength, dwRepetitions = 1;
static HWAVEIN hWaveIn;
static HWAVEOUT hWaveOut;
static PBYTE pBuffer1, pBuffer2, pSaveBuffer, pNewBuffer;
static PWAVEHDR pWaveHdr1, pWaveHdr2;
static TCHAR szOpenError[] = TEXT("Error opening waveform audio!");
static TCHAR szMemError[] = TEXT("Error allocating memory!");
static WAVEFORMATEX waveform;
switch (message)
{
case WM_INITDIALOG:
// Allocate memory for wave header
pWaveHdr1 = (PWAVEHDR)malloc(sizeof(WAVEHDR));
pWaveHdr2 = (PWAVEHDR)malloc(sizeof(WAVEHDR));
// Allocate memory for save buffer
pSaveBuffer = (PBYTE)malloc(1);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_RECORD_BEG:
// Allocate buffer memory
pBuffer1 = (PBYTE)malloc(INP_BUFFER_SIZE);
pBuffer2 = (PBYTE)malloc(INP_BUFFER_SIZE);
if (!pBuffer1 || !pBuffer2)
{
if (pBuffer1) free(pBuffer1);
if (pBuffer2) free(pBuffer2);
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szMemError, szAppName,
MB_ICONEXCLAMATION | MB_OK);
return TRUE;
}
// Open waveform audio for input
waveform.wFormatTag = WAVE_FORMAT_PCM;
waveform.nChannels = 1;
waveform.nSamplesPerSec = 11025;
waveform.nAvgBytesPerSec = 11025;
waveform.nBlockAlign = 1;
waveform.wBitsPerSample = 8;
waveform.cbSize = 0;
if (waveInOpen(&hWaveIn, WAVE_MAPPER, &waveform,
(DWORD)hwnd, 0, CALLBACK_WINDOW))
{
free(pBuffer1);
free(pBuffer2);
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szOpenError, szAppName,
MB_ICONEXCLAMATION | MB_OK);
}
// Set up headers and prepare them
pWaveHdr1->lpData = (LPSTR)pBuffer1;
pWaveHdr1->dwBufferLength = INP_BUFFER_SIZE;
pWaveHdr1->dwBytesRecorded = 0;
pWaveHdr1->dwUser = 0;
pWaveHdr1->dwFlags = 0;
pWaveHdr1->dwLoops = 1;
pWaveHdr1->lpNext = NULL;
pWaveHdr1->reserved = 0;
waveInPrepareHeader(hWaveIn, pWaveHdr1, sizeof(WAVEHDR));
pWaveHdr2->lpData = (LPSTR)pBuffer2;
pWaveHdr2->dwBufferLength = INP_BUFFER_SIZE;
pWaveHdr2->dwBytesRecorded = 0;
pWaveHdr2->dwUser = 0;
pWaveHdr2->dwFlags = 0;
pWaveHdr2->dwLoops = 1;
pWaveHdr2->lpNext = NULL;
pWaveHdr2->reserved = 0;
waveInPrepareHeader(hWaveIn, pWaveHdr2, sizeof(WAVEHDR));
return TRUE;
case IDC_RECORD_END:
// Reset input to return last buffer
bEnding = TRUE;
waveInReset(hWaveIn);
return TRUE;
case IDC_PLAY_BEG:
// Open waveform audio for output
waveform.wFormatTag = WAVE_FORMAT_PCM;
waveform.nChannels = 1;
waveform.nSamplesPerSec = 11025;
waveform.nAvgBytesPerSec = 11025;
waveform.nBlockAlign = 1;
waveform.wBitsPerSample = 8;
waveform.cbSize = 0;
if (waveOutOpen(&hWaveOut, WAVE_MAPPER, &waveform,
(DWORD)hwnd, 0, CALLBACK_WINDOW))
{
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szOpenError, szAppName,
MB_ICONEXCLAMATION | MB_OK);
}
return TRUE;
case IDC_PLAY_PAUSE:
// Pause or restart output
if (!bPaused)
{
waveOutPause(hWaveOut);
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Resume"));
bPaused = TRUE;
}
else
{
waveOutRestart(hWaveOut);
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Pause"));
bPaused = FALSE;
}
return TRUE;
case IDC_PLAY_END:
// Reset output for close preparation
bEnding = TRUE;
waveOutReset(hWaveOut);
return TRUE;
case IDC_PLAY_REV:
// Reverse save buffer and play
bReverse = TRUE;
ReverseMemory(pSaveBuffer, dwDataLength);
SendMessage(hwnd, WM_COMMAND, IDC_PLAY_BEG, 0);
return TRUE;
case IDC_PLAY_REP:
// Set infinite repetitions and play
dwRepetitions = -1;
SendMessage(hwnd, WM_COMMAND, IDC_PLAY_BEG, 0);
return TRUE;
case IDC_PLAY_SPEED:
// Open waveform audio for fast output
waveform.wFormatTag = WAVE_FORMAT_PCM;
waveform.nChannels = 1;
waveform.nSamplesPerSec = 22050;
waveform.nAvgBytesPerSec = 22050;
waveform.nBlockAlign = 1;
waveform.wBitsPerSample = 8;
waveform.cbSize = 0;
if (waveOutOpen(&hWaveOut, 0, &waveform, (DWORD)hwnd, 0,
CALLBACK_WINDOW))
{
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szOpenError, szAppName,
MB_ICONEXCLAMATION | MB_OK);
}
return TRUE;
}
break;
case MM_WIM_OPEN:
// Shrink down the save buffer
pSaveBuffer = (PBYTE)realloc(pSaveBuffer, 1);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REV), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REP), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_SPEED), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_RECORD_END));
// Add the buffers
waveInAddBuffer(hWaveIn, pWaveHdr1, sizeof(WAVEHDR));
waveInAddBuffer(hWaveIn, pWaveHdr2, sizeof(WAVEHDR));
// Begin sampling
bRecording = TRUE;
bEnding = FALSE;
dwDataLength = 0;
waveInStart(hWaveIn);
return TRUE;
case MM_WIM_DATA:
// Reallocate save buffer memory
pNewBuffer = (PBYTE)realloc(pSaveBuffer, dwDataLength + ((PWAVEHDR)lParam)->dwBytesRecorded);
if (pNewBuffer == NULL)
{
waveInClose(hWaveIn);
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szMemError, szAppName,
MB_ICONEXCLAMATION | MB_OK);
return TRUE;
}
pSaveBuffer = pNewBuffer;
CopyMemory(pSaveBuffer + dwDataLength, ((PWAVEHDR)lParam)->lpData,
((PWAVEHDR)lParam)->dwBytesRecorded);
dwDataLength += ((PWAVEHDR)lParam)->dwBytesRecorded;
if (bEnding)
{
waveInClose(hWaveIn);
return TRUE;
}
// Send out a new buffer
waveInAddBuffer(hWaveIn, (PWAVEHDR)lParam, sizeof(WAVEHDR));
return TRUE;
case MM_WIM_CLOSE:
// Free the buffer memory
waveInUnprepareHeader(hWaveIn, pWaveHdr1, sizeof(WAVEHDR));
waveInUnprepareHeader(hWaveIn, pWaveHdr2, sizeof(WAVEHDR));
free(pBuffer1);
free(pBuffer2);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_RECORD_BEG));
if (dwDataLength > 0)
{
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REP), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REV), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_SPEED), TRUE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
}
bRecording = FALSE;
if (bTerminating)
SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);
return TRUE;
case MM_WOM_OPEN:
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REP), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REV), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_SPEED), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_END));
// Set up header
pWaveHdr1->lpData = (LPSTR)pSaveBuffer;
pWaveHdr1->dwBufferLength = dwDataLength;
pWaveHdr1->dwBytesRecorded = 0;
pWaveHdr1->dwUser = 0;
pWaveHdr1->dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP;
pWaveHdr1->dwLoops = dwRepetitions;
pWaveHdr1->lpNext = NULL;
pWaveHdr1->reserved = 0;
// Prepare and write
waveOutPrepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
waveOutWrite(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
bEnding = FALSE;
bPlaying = TRUE;
return TRUE;
case MM_WOM_DONE:
waveOutUnprepareHeader(hWaveOut, pWaveHdr1, sizeof(WAVEHDR));
waveOutClose(hWaveOut);
return TRUE;
case MM_WOM_CLOSE:
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REV), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_REP), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_SPEED), TRUE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Pause"));
bPaused = FALSE;
dwRepetitions = 1;
bPlaying = FALSE;
if (bReverse)
{
ReverseMemory(pSaveBuffer, dwDataLength);
bReverse = FALSE;
}
if (bTerminating)
SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);
return TRUE;
case WM_SYSCOMMAND:
switch (LOWORD(wParam))
{
case SC_CLOSE:
if (bRecording)
{
bTerminating = TRUE;
bEnding = TRUE;
waveInReset(hWaveIn);
return TRUE;
}
if (bPlaying)
{
bTerminating = TRUE;
bEnding = TRUE;
waveOutReset(hWaveOut);
return TRUE;
}
free(pWaveHdr1);
free(pWaveHdr2);
free(pSaveBuffer);
EndDialog(hwnd, 0);
return TRUE;
}
break;
}
return FALSE;
}
RECORD.RC (excerpts)
// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"
/
//
// Dialog
//
RECORD DIALOG DISCARDABLE 100, 100, 152, 74
STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Waveform Audio Recorder"
FONT 8, "MS Sans Serif"
BEGIN
PUSHBUTTON "Record", IDC_RECORD_BEG, 28, 8, 40, 14
PUSHBUTTON "End", IDC_RECORD_END, 76, 8, 40, 14, WS_DISABLED
PUSHBUTTON "Play", IDC_PLAY_BEG, 8, 30, 40, 14, WS_DISABLED
PUSHBUTTON "Pause", IDC_PLAY_PAUSE, 56, 30, 40, 14, WS_DISABLED
PUSHBUTTON "End", IDC_PLAY_END, 104, 30, 40, 14, WS_DISABLED
PUSHBUTTON "Reverse", IDC_PLAY_REV, 8, 52, 40, 14, WS_DISABLED
PUSHBUTTON "Repeat", IDC_PLAY_REP, 56, 52, 40, 14, WS_DISABLED
PUSHBUTTON "Speedup", IDC_PLAY_SPEED, 104, 52, 40, 14, WS_DISABLED
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ generated include file.
// Used by record1.rc
#define IDC_RECORD_BEG 1000
#define IDC_RECORD_END 1001
#define IDC_PLAY_BEG 1002
#define IDC_PLAY_PAUSE 1003
#define IDC_PLAY_END 1004
#define IDC_PLAY_REV 1005
#define IDC_PLAY_REP 1006
#define IDC_PLAY_SPEED 1007
RECORD.RC 和 RESOUCE.H 文件也会在 RECORD2 和 RECORD3 程序中用到。
RECORD1 程序窗口中有 8 个按钮。当你第一次运行 RECORD1 程序时,只有 Record 按钮是可用的。当你按下 Record 按钮,录音就开始了。Record 按钮编程禁用状态,而 End 按钮变成了可用状态。按下 End 按钮就会停止录音。这时,Play、Reverse、Repeat 和 Speedup 按钮就会变成可用状态。按下其中任何一个按钮就会开始播放录音:Play 是正常播放,Reverse 是反向播放,Repeat 是不停地重复播放(就像循环播放磁带一样),Speedup 是以两倍正常速度播放。你可以再次按下 End 按钮来停止播放,或者按下 Pause 按钮来暂停播放。当 Pause 按钮被按下后,它就会变成 Resume 按钮,用以恢复播放。如果你录下另一段声音,它会替代内存中现有的声音。
不管是什么时候,只有那些有效的按钮才会处于可用状态。这就需要在 RECORD1 的源代码中有很多的 EnableWindow 函数调用,不过这样程序就不需要在按下某一个按钮时再检查它是不是有效了。当然了,这也让程序的操作变得更直观了。
RECORD1 程序还是用了一些捷径来简化代码。首先,如果安装了多个波形音频硬件,RECORD1 只使用默认的那一个。其次,程序总是使用 11.025kHz 的采样频率和 8 位的采样大小来录音和播放,而不管是否有更高的采样频率和采样大小可用。唯一的例外是快速播放功能,这时,RECORD1 以 22.050kHz 的采样频率播放,也就是以 2 倍于正常速度和比正常频率高八度的频率播放。
录音时需要打开波形音频硬件来接收输入,且把缓冲区数据传给 API 函数来接收声音数据。
RECORD1 程序使用了几个内存块。其中有三个是很小的(至少在初始化时很小),它们是在处理 DlgProc 的 WM_INITDIALOG 消息时分配的。程序分配了两个 WAVEHDR 结构,pWaveHdr1 和 pWaveHdr2 分别是它们的指针。这些结构是用来把缓冲区传给波形 API 的。pSaveBuffer 指针指向存储所有录下来的声音的缓冲区。一开始它只分配了一个字节,在后来的录音过程中,它的大小会不断增加以容纳所有的声音数据。(如果你录音了很长时间,RECORD1 发现内存不足时,它可以很好地处理这种情况,这样至少可以让你能够播放已经被存储的部分声音。)我讲这个缓冲区称为“保存缓冲区”,因为它就是用来保存已经收集到的声音数据的。还有两块缓冲区大小是 16K,由 pBuffer1 和 pBuffer2 来指向,它们是在录音时分配的,用以接收声音数据。当录音结束时,这两个缓冲区就会被释放掉。
RECORD1 的 8 个按钮中的每一个都会产生一条 WM_COMMAND 消息发送给 DlgProc 函数,DlgProc 是 RECORD1 窗口的对话框过程。最开始,只有 Record 按钮处于可用状态。按下这个按钮会产生一条 WM_COMMAND 消息,它的 wParam 参数是 IDC_RECORD_BEG 为了处理这条消息,RECORD1 分配了两个 16K 的缓冲区来接收声音数据,初始化 WAVEFORMATEX 结构的各个字段,并把它传给 waveInOpen 函数,最后设置好两个 WAVEHDR 结构。
waveInOpen 函数会产生一条 MM_WIM_OPEN 消息。在这个消息中,RECORD1 把保存缓冲区缩小到 1 字节长来准备接收数据。(当然,在第一次录音时,保存缓冲区是 1 字节长,但是在后面的录音过程中,它可能会变得很大。)在处理 MM_WIM_OPEN 消息时,RECORD1 还会启用或禁用相应的按钮。接着,程序把两个 WAVEHDR 结构和缓冲区通过 waveInAddBuffer 传给 API,设置一些标志,然后调用 waveInStart 来开始录音。
在 11.025kHz 的采样频率和 8 位采样大小的情况下,16K 的缓冲区大约会在 1.5 秒钟内被填满。这时,RECORD1 会收到一条 MM_WIM_DATA 消息。收到这个消息后,程序就会基于 dwDataLength 变量的值和 WAVEHDR 结构中的 dwBytesRecorded 字段的值来重新分配保存缓冲区。如果分配失败,RECORD1 就会调用 waveInClose 函数来停止录音。
如果重新分配成功,RECORD1 就会把数据从 16K 的缓冲区复制到保存缓冲区中。然后它就会再次调用 waveInAddBuffer。这个过程会一直持续下去,直到 RECORD1 没有足够的内存或者用户按下 End 按钮为止。
End 按钮会产生一条 WM_COMMAND 消息,它的 wParam 是 IDC_RECORD_END。处理这个消息很简单,RECORD1 把 bEnding 标志设置为 TRUE,然后 调用 waveInReset 函数。此函数会停止录音并产生一条 MM_WIM_DATA 消息,该消息内含了那个被部分填充的缓冲区。RECORD1 对这个最后的 MM_WIM_DATA 消息的处理和前面一样,只是最后会调用 waveInClose 函数来关闭波形音频输入设备。
waveInClose 函数会产生一条 MM_WIM_CLOSE 消息。RECORD1 在收到这个消息时会释放 16K 的输入缓冲区,然后启用或禁用适当的按钮。特别是,如果保存缓冲区中有数据时(一般情况下都是如此,除非第一次的重新分配内存就失败了),Play 按钮就会被启用。
在一段录音结束后,保存缓冲区中会存有收集到的声音数据。当用户按下 Play 按钮时,DlgProc 就会收到一条 WM_COMMAND 消息,它的 wParam 是 IDC_PLAY_BEG。收到这个消息后,程序就会初始化 WAVEFORMATEX 结构的各个字段,然后调用 waveOutOpen 函数。
调用 waveOutOpen 函数同样会产生一条 MM_WOM_OPEN 消息。收到这个消息后,RECORD1 会启用或禁用相应的按钮(只允许使用 Pause 和 End 按钮),然后用保存缓冲区初始化 WAVEHDR 结构的字段,再调用 waveOutPrepareHeader 来准备好这个结构,最后调用 waveOutWrite 函数来开始播放。
一般情况下,声音会持续播放直到缓冲区中的所有数据都播放完为止。这时会产生一条 MM_WOM_DONE 消息。如果还有其他的缓冲区需要播放,那么程序可以再把它们传给 API。因为 RECORD1 只播放一个大缓冲区,所以它就简单地清除 WAVEHDR 结构然后调用 waveOutClose 函数。这个函数函数产生一条 MM_WOM_CLOSE 消息。收到这个消息后,RECORD1 会启用或禁用相应的按钮,允许声音被重新播放或是允许录制一段新的录音。
程序还包括了第二个 End 按钮,以便让用户在保存缓冲区没有完全播放完时可以随时停止播放。这个 End 按钮产生一条 WM_COMMAND 消息,它的 wParam 是 IDC_PLAY_END。收到这个消息后,程序就会调用 waveOutReset 函数。此函数会产生一条 MM_WOM_DONE 消息,该消息会被正常处理。
RECORD1 的窗口还包含了一个 Pause 按钮。处理这个按钮很容易。第一次它被按下时 RECORD1 会调用 waveOutPause 函数来暂停播放,然后把按钮的文本设置成 Resume。按下 Resume 按钮就会调用 waveOutRestart 来恢复播放。
为了让程序更有趣一些,程序还包括了 Reverse、Repeat 和 Speedup 按钮。这些按钮会产生 WM_COMMAND 消息,其 wParam 值分别是 IDC_PLAY_REV,IDC_PLAY_REP 和 IDC_PLAY_SPEED。
倒放声音需要把保存缓冲区中的字节倒序排列,然后正常播放就可以了。RECORD1 包含了一个名为 ReverseMemory 的小函数来把字节倒序排列。程序在处理相应的 WM_COMMAND 消息时,先调用这个函数,然后再进行播放。同样,在处理 MM_WOM_CLOSE 消息的最后要再次调用它以使缓冲区恢复原样。
Repeat 按钮会一遍又一遍地播放声音。这也不复杂,因为 API 包括了循环播放的功能。程序只需要把 WAVEHDR 结构的 dwLoops 字段设置成需要重复的次数,把 dwFlags 字段再循环开始的缓冲区内设置为 WHDR_BEGINLOOP,在最后的缓冲区内设置为 WHDR_ENDLOOP 就可以了。因为 RECORD1 只用了一个缓冲区来播放声音,所以这两个标志就组合在一起设置在了 dwFlags 字段中。
以正常速度的两倍播放也是很容易的。在初始化 WAVEFORMATEX 结构来准备打开波形音频设备时,把它的 nSamplesPerSec 和 nAvgBytesPerSec 字段设置为 22050 而不是 11025 即可。
22.2.7 使用 MCI 的录音机
你可能和我一样,会觉得 RECORD1 相当地复杂。处理波形音频函数调用和它们产生的消息之间的交互需要一些特别的技巧,而且在这期间,还要应付可能出现的内存不足的情况。但是这也许就是它被叫做“底层”接口的原因。正如我在本章前面部分提到的,Windows 还包括了高层的媒体控制接口(Media Control Interface, MCI)。
对波形音频来说,底层接口和 MCI 的主要区别是 MCI 把声音数据录制到一个波形文件中,在播放时再从文件中读取出来。这使得在 RECORD1 中实现的“特殊效果”变得困难了,因为你必须先读入文件,对它进行一些操作,再把它写入回去才能播放声音。这是一个典型的功能和易用性之间的取舍。底层接口提供了灵活性,但是 MCI(在大多数情况下)更容易使用。
第一种形式用消息和数据结构来发送命令到多媒体设备并从设备那里接收信息。第二种形式使用 ASCII 文本字符串。这种基于文本的接口最初是为了能用简单的脚本语言控制多媒体设备而设计的。但是它也提供了非常简单的交互控制,就像本章前面的 TESTMCI 程序演示的一样。
图 22-4 所示的 RECORD2 程序用 MCI 的消息和数据结构来实现了另外一个数字音频录音机和播放器。虽然它用到了和 RECORD1 同样的对话框模板,但是并没有实现三个特殊的功能按钮。
/*--------------------------------------------------------
RECORD2.C -- Waveform Audio Recorder
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include "resource.h"
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("Record2");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
if (-1 == DialogBox(hInstance, TEXT("Record"), NULL, DlgProc))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
}
return 0;
}
void ShowError(HWND hwnd, DWORD dwError)
{
TCHAR szErrorStr[1024];
mciGetErrorString(dwError, szErrorStr, sizeof(szErrorStr) / sizeof(TCHAR));
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szErrorStr, szAppName, MB_OK | MB_ICONEXCLAMATION);
}
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bRecording, bPlaying, bPaused;
static TCHAR szFileName[] = TEXT("record2.wav");
static WORD wDeviceID;
DWORD dwError;
MCI_GENERIC_PARMS mciGeneric;
MCI_OPEN_PARMS mciOpen;
MCI_PLAY_PARMS mciPlay;
MCI_RECORD_PARMS mciRecord;
MCI_SAVE_PARMS mciSave;
switch (message)
{
case WM_COMMAND:
switch (wParam)
{
case IDC_RECORD_BEG:
// Delete existing waveform file
DeleteFile(szFileName);
// Open waveform audio
mciOpen.dwCallback = 0;
mciOpen.wDeviceID = 0;
mciOpen.lpstrDeviceType = TEXT("waveaudio");
mciOpen.lpstrElementName = TEXT("");
mciOpen.lpstrAlias = NULL;
dwError = mciSendCommand(0, MCI_OPEN,
MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
(DWORD)(LPMCI_OPEN_PARMS)&mciOpen);
if (dwError != 0)
{
ShowError(hwnd, dwError);
return TRUE;
}
// Save the Device ID
wDeviceID = mciOpen.wDeviceID;
// Begin recording
mciRecord.dwCallback = (DWORD)hwnd;
mciRecord.dwFrom = 0;
mciRecord.dwTo = 0;
mciSendCommand(wDeviceID, MCI_RECORD, MCI_NOTIFY,
(DWORD)(LPMCI_RECORD_PARMS)&mciRecord);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_RECORD_END));
bRecording = TRUE;
return TRUE;
case IDC_RECORD_END:
// Stop recording
mciGeneric.dwCallback = 0;
mciSendCommand(wDeviceID, MCI_STOP, MCI_WAIT,
(DWORD)(LPMCI_GENERIC_PARMS)&mciGeneric);
// Save the file
mciSave.dwCallback = 0;
mciSave.lpfilename = szFileName;
mciSendCommand(wDeviceID, MCI_SAVE, MCI_WAIT | MCI_SAVE_FILE,
(DWORD)(LPMCI_SAVE_PARMS)&mciSave);
// Close the waveform device
mciSendCommand(wDeviceID, MCI_CLOSE, MCI_WAIT,
(DWORD)(LPMCI_GENERIC_PARMS)&mciGeneric);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
bRecording = FALSE;
return TRUE;
case IDC_PLAY_BEG:
// Open waveform audio
mciOpen.dwCallback = 0;
mciOpen.wDeviceID = 0;
mciOpen.lpstrDeviceType = NULL;
mciOpen.lpstrElementName = szFileName;
mciOpen.lpstrAlias = NULL;
dwError = mciSendCommand(0, MCI_OPEN,
MCI_WAIT | MCI_OPEN_ELEMENT,
(DWORD)(LPMCI_OPEN_PARMS)&mciOpen);
if (dwError != 0)
{
ShowError(hwnd, dwError);
return TRUE;
}
// Save the Device ID
wDeviceID = mciOpen.wDeviceID;
// Begin playing
mciPlay.dwCallback = (DWORD)hwnd;
mciPlay.dwFrom = 0;
mciPlay.dwTo = 0;
mciSendCommand(wDeviceID, MCI_PLAY, MCI_NOTIFY,
(DWORD)(LPMCI_PLAY_PARMS)&mciPlay);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), TRUE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_END));
bPlaying = TRUE;
return TRUE;
case IDC_PLAY_PAUSE:
if (!bPaused)
// Pause the play
{
mciGeneric.dwCallback = 0;
mciSendCommand(wDeviceID, MCI_PAUSE, MCI_WAIT,
(DWORD)(LPMCI_GENERIC_PARMS)&mciGeneric);
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Resume"));
bPaused = TRUE;
}
else
// Begin playing again
{
mciPlay.dwCallback = (DWORD)hwnd;
mciPlay.dwFrom = 0;
mciPlay.dwTo = 0;
mciSendCommand(wDeviceID, MCI_PLAY, MCI_NOTIFY,
(DWORD)(LPMCI_PLAY_PARMS)&mciPlay);
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Pause"));
bPaused = FALSE;
}
return TRUE;
case IDC_PLAY_END:
// Stop and close
mciGeneric.dwCallback = 0;
mciSendCommand(wDeviceID, MCI_STOP, MCI_WAIT,
(DWORD)(LPMCI_GENERIC_PARMS)&mciGeneric);
mciSendCommand(wDeviceID, MCI_CLOSE, MCI_WAIT,
(DWORD)(LPMCI_GENERIC_PARMS)&mciGeneric);
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
bPlaying = FALSE;
bPaused = FALSE;
return TRUE;
}
break;
case MM_MCINOTIFY:
switch (wParam)
{
case MCI_NOTIFY_SUCCESSFUL:
if (bPlaying)
SendMessage(hwnd, WM_COMMAND, IDC_PLAY_END, 0);
if (bRecording)
SendMessage(hwnd, WM_COMMAND, IDC_RECORD_END, 0);
return TRUE;
}
break;
case WM_SYSCOMMAND:
switch (wParam)
{
case SC_CLOSE:
if (bRecording)
SendMessage(hwnd, WM_COMMAND, IDC_RECORD_END, 0L);
if (bPlaying)
SendMessage(hwnd, WM_COMMAND, IDC_PLAY_END, 0L);
EndDialog(hwnd, 0);
return TRUE;
}
break;
}
return FALSE;
}
RECORD2 只用到了两个 MCI 函数,最重要的是如下这个函数:
error = mciSendCommand(wDeviceID, message, dwFlags, dwParams);
它的第一个参数是设备的标识(ID),使用数值来表示。可以像使用一个句柄一样使用这个 ID。在打开设备时就会得到这个 ID,然后就可以在后续的 mciSendCommand 调用中使用它了。第二个参数是一个以 MCI 为前缀的常数。这些常数被称作 MCI 命令消息(MCI command message),RECORD2 演示了其中的 7 个常数:MCI_OPEN、MCI_RECORD、MCI_STOP、MCI_SAVE、MCI_PLAY、MCI_PAUSE 和 MCI_CLOSE。
dwFlags 参数一般由 0 个或多个位标识常数用 C 语言的按位或操作结合而成。通常,这些位标识代表不同的选项。有些选项只针对某些特定的 MCI 命令消息,有些则是所有的消息通用的。dwParam 参数一般是一个指向某个数据结构的长指针,用以指定一些选项和从设备中获取信息。很多 MCI 消息都有其独特的数据结构。
mciSendCommand 函数在成功时会返回 0,失败时返回一个错误代码。如果要向用户报告这个错误,你可以使用下面的函数来获取一个描述这个错误的文本字符串:
mciGetErrorString(error, szBuffer, dwLength);
这和 TESTMCI 程序中使用的函数相同。
当用户按下 Record 按钮时,RECORD2 的窗口过程就会收到一条 WM_COMMAND 消息,其 wParam 等于 IDC_RECORD_BEG。RECORD2 首先打开设备,这需要设置 MCI_OPEN_PARAMS 数据结构的各个字段,然后使用 MCI_OPEN 命令消息来调用 mciSendCommand 函数。对录音来说,应将 lpstrDeviceType 字段设置为字符串“waveaudio”来指明设备的类型。还要将 lpstrElementName 字段设置为一个长度为 0 的字符串。MCI 驱动程序会使用默认的采样频率和采样大小,但是你可以用 MCI_SET 命令来改变它。在录音过程中,声音数据存储在硬盘上的一个临时文件中,最终会被转移到一个标准的波形文件里。我会在本章的稍后部分讨论波形文件的格式。播放声音时,MCI 使用波形文件中定义的采样频率和采样大小。
如果 RECORD2 无法打开一个设备,它会用 mciGetErrorString 和 MessageBox 函数来告诉用户问题所在。如果没有出错的话,在 mciSendCommand 返回时,MCI_OPEN_PARAMS 结构中的 wDeviceID 字段就会包含设备 ID,它会被用在后面的函数调用中。
为了开始录音,RECORD2 用 MCI_RECORD 命令消息和 MCI_WAVE_RECORD_PARMS 数据结构来调用 mciSendCommand 函数。你也可以设置这个结构的 dwFrom 和 dwTo 字段(并使用一些位标识来表示这两个字段已被设置),来把一段声音插入到一个现有的波形文件中,文件名由 MCI_OPEN_PARMS 结构中的 lpstrElementName 字段指定。默认情况下,新的声音数据会被插入到一个现有文件的开始部分。
RECORD2 把 MCI_WAVE_RECORD_PARMS 结构的 dwCallback 字段设置为程序的窗口句柄,并且在 mciSendCommand 调用中包括了一个 MCI_NOTIFY 标志。这样,当录音完成时,就会发送一条通知消息(Notification Message)给窗口过程。我会很快讨论到这个通知消息。
当录音结束时,按下第一个 End 按钮可以停止录音。这会产生一条 WM_COMMAND 消息,其 wParam 参数是 IDC_RECORD_END。窗口过程对这个消息的处理是调用 mciSendCommand 函数三次:使用 MCI_STOP 命令消息停止录音,使用 MCI_SAVE 命令消息把声音数据从临时文件传送到由 MCI_SAVE_PARMS 结构指定的文件(record2.wav)中,然后使用 MCI_CLOSE 命令消息删除所有的临时文件和内存并关闭设备。
播放声音时,MCI_OPEN_PARMS 结构的 lpstrElementName 字段被设置为文件名 “record2.wav”。mciSendCommand 的第三个参数中的 MCI_OPEN_ELEMENT 标志指明 lpstrElementName 字段是一个有效的文件名。MCI 从文件的扩展名 .WAV 就可以知道要打开的是一个波形音频设备。如果有多个波形硬件存在,则 MCI 会打开第一个设备。(也可以通过设置 MCI_OPEN_PARMS 结构的 lpstrDeviceType 字段来使用其他的波形设备。)
播放声音涉及用 MCI_PLAY 命令消息和一个 MCI_PLAY_PARMS 结构来调用 mciSendCommand 函数。波形文件的任何部分都可以被播放,但 RECORD2 选择了播放全部声音。
MCI_GENERIC_PARMS 结构可以用在所有不需要特殊信息(可以含有一个可选的窗口句柄来接收通知)的消息中。如果播放已经被暂停了,这个按钮就会用 MCI_PLAY 命令消息调用 mciSendCommand 函数来恢复播放。
播放也可以通过按下第二个 End 按钮来终止。这会产生一条 WM_COMMAND 消息,它的 wParam 等于 IDC_PLAY_END。窗口过程对此的处理是调用 mciSendCommand 两次,首先是使用 MCI_STOP 命令消息,然后是 MCI_CLOSE 命令消息。
现在有一个问题:虽然可以通过按下 End 按钮手动终止播放,但你也可能想播放整个声音文件。那么程序怎么知道文件播放完了呢?这就是 MCI 通知消息的作用了。
当用 MCI_RECORD 和 MCI_PLAY 消息调用 mciSendCommand 时,RECORD2 用了一个 MCI_NOTIFY 标志并且把数据结构的 dwCallback 字段设置为程序的窗口句柄。这就会在特定情况下产生一条通知消息,称作 MM_MCINOTIFY,该消息会被发送到窗口过程,这个消息的 wParam 参数是状态码,lParam 参数是设备 ID。
当使用 MCI_STOP 或 MCI_PAUSE 命令消息调用 mciSendCommand 时,你将收到一条 MM_MCINOTIFY 消息,它的 wParam 等于 MCI_NOTIFY_ABORTED。这发生在你按下 Pause 按钮或任何一个 End 按钮的时候。RECORD2 可以忽略这个消息,因为它已经适当地处理这些按钮。在播放时,如果声音文件播放完了,你会收到一条 MM_MCINOTIFY 消息,它的 wParam 等于 MCI_NOTIFY_SUCCESSFUL。为了处理这个消息,窗口过程给自己发送一条 WM_COMMAND 消息,它的 wParam 等于 IDC_PLAY_END,来模拟 End 按钮被按下时的情况。之后窗口过程对这个消息的处理就是正常地停止播放和关闭设备。
在录音期间,如果用于存储临时声音文件的硬盘空间不足时,你会收到一条 MM_MCINOTIFY 消息,它的 wParam 等于 MCI_NOTIFY_SUCCESSFUL。(严格地讲,我不会称这种情况为 SUCCESSFUL(成功的),但 MCI 的确就是这么命名的。)对此,窗口过程会给自己发送一条 WM_COMMAND 消息,它的 wParam 等于 IDC_RECORD_END。窗口过程会像通常那样停止录音、保存文件,并关闭设备。
22.2.8 用 MCI 命令字符串的方法
有一段时间,Windows 多媒体接口曾包括一个名为 mciExecute 的函数,它的语法如下:
bSuccess = mciExecute(szCommand);
这个函数唯一的参数就是一个 MCI 命令串。这个函数返回一个布尔值——如果成功,则结果非 0,失败则结果为 0。mciExecute 函数在功能上等价于用 NULL 或 0 作为最后三个参数来调用 mciSendString (在 TESTMCI 中用到的基于字符串的 MCI 函数),并在发生错误时用 mciGetErrorString 和 MessageBox 函数来报告。
虽然 mciExecute 已经不再是 API 的一部分了,但我还是包括了一个使用该函数的数字录音机和播放器程序,那就是 RECORD3,如图 22-5 所示。同 RECORD2 一样,这个程序使用了 RECORD1 中的 RECORD.RC 和 RESOURCE.H 文件。
/*--------------------------------------------------------
RECORD3.C -- Waveform Audio Recorder
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include "resource.h"
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("Record3");
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
if (-1 == DialogBox(hInstance, TEXT("Record"), NULL, DlgProc))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
}
return 0;
}
BOOL mciExecute(LPCTSTR szCommand)
{
MCIERROR error;
TCHAR szErrorStr [1024];
if (error = mciSendString(szCommand, NULL, 0, NULL))
{
mciGetErrorString(error, szErrorStr,
sizeof(szErrorStr) / sizeof(TCHAR));
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(NULL, szErrorStr, TEXT("MCI Error"),
MB_OK | MB_ICONEXCLAMATION);
}
return error == 0;
}
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static BOOL bRecording, bPlaying, bPaused;
switch (message)
{
case WM_COMMAND:
switch (wParam)
{
case IDC_RECORD_BEG:
// Delete existing waveform file
DeleteFile(TEXT("record3.wav"));
// Open waveform audio and record
if (!mciExecute(TEXT("open new type waveaudio alias mysound")))
return TRUE;
mciExecute(TEXT("record mysound"));
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_RECORD_END));
bRecording = TRUE;
return TRUE;
case IDC_RECORD_END:
// Stop, Save, and close recording
mciExecute(TEXT("stop mysound"));
mciExecute(TEXT("save mysound record3.wav"));
mciExecute(TEXT("close mysound"));
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
bRecording = FALSE;
return TRUE;
case IDC_PLAY_BEG:
// Open waveform audio and play
if (!mciExecute(TEXT("open record3.wav alias mysound")))
return TRUE;
mciExecute(TEXT("play mysound"));
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), TRUE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_END));
bPlaying = TRUE;
return TRUE;
case IDC_PLAY_PAUSE:
if (!bPaused)
// Pause the play
{
mciExecute(TEXT("pause mysound"));
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Resume"));
bPaused = TRUE;
}
else
// Begin playing again
{
mciExecute(TEXT("play mysound"));
SetDlgItemText(hwnd, IDC_PLAY_PAUSE, TEXT("Pause"));
bPaused = FALSE;
}
return TRUE;
case IDC_PLAY_END:
// Stop and close
mciExecute(TEXT("stop mysound"));
mciExecute(TEXT("close mysound"));
// Enable and disable buttons
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_RECORD_END), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_BEG), TRUE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_PAUSE), FALSE);
EnableWindow(GetDlgItem(hwnd, IDC_PLAY_END), FALSE);
SetFocus(GetDlgItem(hwnd, IDC_PLAY_BEG));
bPlaying = FALSE;
bPaused = FALSE;
return TRUE;
}
break;
case WM_SYSCOMMAND:
switch (wParam)
{
case SC_CLOSE:
if (bRecording)
SendMessage(hwnd, WM_COMMAND, IDC_RECORD_END, 0L);
if (bPlaying)
SendMessage(hwnd, WM_COMMAND, IDC_PLAY_END, 0L);
EndDialog(hwnd, 0);
return TRUE;
}
break;
}
return FALSE;
}
如果比较基于消息的 MCI 接口和基于文本的 MCI 接口,会发现它们对应得很好。可以很容易地猜出 MCI 就是把命令串翻译成对应的命令消息和数据结构。RECORD3 也可以像 RECORD2 一样使用 MM_MCINOTIFY 消息,但它选择不用——mciExecute 函数就是这么实现的。这样做的缺点是程序不知道什么时候波形文件播放结束,所以,按钮不会自动地改变状态。必须手动按下 End 按钮,以便让程序知道准备再次录音或播放。
请注意在 MCI 的 open 命令中用到的 alias 关键词。这个关键词允许所有随后的 MCI 命令使用别名来指明设备。
22.2.9 波形音频文件格式
如果在使用十六进制来显示数据的转储程序中查看未被压缩的(也就是 PCM 方式的) .WAV 文件,你将发现它们的格式如图 22-6 所示。
偏 移 量 | 字 节 | 数 据 |
0000 | 4 | "RIFF" |
0004 | 4 | 波形数据块的大小(文件大小减去 8) |
0008 | 4 | "WAVE" |
000C | 4 | "fmt " |
0010 | 4 | 格式数据块的大小(16 字节) |
0014 | 2 | wf.wFormatTag = WAVE_FORMAT_PCM = 1 |
0016 | 2 | wf.nChannels |
0018 | 4 | wf.nSamplesPerSec |
001C | 4 | wf.nAvgBytesPerSec |
0020 | 2 | wf.nBlockAlign |
0022 | 2 | wf.wBitsPerSample |
0024 | 4 | "data" |
0028 | 4 | 波形数据的大小 |
002C | | 波形数据 |
该格式是 RIFF(Resource Interchange File Format,资源交换文件)格式的一个例子。
RIFF 是一种能够包括所有多媒体数据的文件格式。它使用标记型(tagged)文件格式,也就是说是文件是由很多数据“块” (chunk)组成的。
每一块数据由以下两个值来标识:
首先是块的名称,它是由 4 个 ASCII 字符组成;
接着是块长度,由 4 个字节 32 位组成。块长度的数值不包括块名字和长度所需要的 8 个字节。
波形音频文件以字符串 “RIFF” 开始,表明它是一个 RIFF 文件。后面紧跟着 32 位数值表示块的长度,也就是文件剩下的大小,或者可以说是文件大小减去 8 个字节。
块数据从字符串 “WAVE” 开始,指明它是一个波形音频块。其后是字符串"fmt "——注意最后有一个空格使得这个字符串为 4 个字符长——它指明这个次级块所包含的波形音频数据的格式。"fmt "后面就是格式。"fmt "后面就是格式信息的长度,在这里,格式信息有 16 个字节长。格式信息就是 WAVEFORMATEX 结构的前 16 个字节,或者,像最初定义的一样,是一个 PCMWAVEFORMATE 结构,其中包含一个 WAVEFORMAT 结构。
nChannels 字段的值是 1 或 2,用来表示是单声道还是立体声。nSamplesPerSec 字段表示每秒的采样数量,标准值是 11025、22025 或 44100。nAvgBytesPerSec 字段表示每秒产生的字节数,其数值是先将采样率乘以声道数乘以每个采样的大小(以位计算),再除以 8,然后向上取整。标准的采样大小是 8 位或 16 位。nBlockAlign 是先将声道数乘以每个采样的大小(以位计算)再除以 8,然后向上取整。格式的最后是 wBitsPerSample 字段,它的值是声道数乘以每个采样的大小(以位计算)。
对立体声波形数据来说,每个样本先包括左声道的数值然后是右声道的数值。
如果采样大小为 8 位或更少,那么采样值将被解释为无符号整数。比如,对 8 位的采样大小,无声等于一个 0x80。如果采样大小为 9 位或更多时,采样值被解释为有符号整数,也就是说,无声等于 0。
读取基于标记的文件的一个重要规则就是要忽略不准备处理的块。虽然波形音频文件要求依次有 "fmf " 和 "data" 次级块,但它还可能包含其他次级块。特别是,波形音频文件可以包含一个标记为 "INFO" 的次级块,该次级块中的再次级块可以提供关于波形音频文件的某些信息。
22.2.10 尝试使用加法合成
很多年来(至少可以追溯到毕达哥斯拉的时代)人们就一直试图分析乐音。最初,它看起来似乎非常简单,但是越到后来越复杂。请担待一下,我可能要重复一些我已经讲过的关于声音的知识。
除了某些打击乐,乐音都有一个特定的音高或频率。这个频率可能横跨人对声音的感知频谱,即 20Hz~20000Hz。例如,钢琴音符的频率范围在 27.5Hz~4186Hz 之间。乐音的另一个特征是音量或声音的大小。这对应于产生这个声音的波形的振幅。音量的变化用分贝来表示。到目前为止,一切都很好。
但是乐音还有一个称为“音色”(timbre)的难以 处理的特征。简单地说,音色就是可以让我们区别演奏同一音高、同样音量的钢琴、小提琴和喇叭的那种声音特性。
法国数学家傅里叶(Fourier)发现了所有周期性波形信号——不管怎么复杂——都能由一系列正弦波的叠加来表示,而这些正弦波的频率都是一个基本频率的整数倍。其基本频率,也称为一次谐波,决定了整个波形信号的周期。一次泛音,也称为二次谐波,其频率是基本频率的两倍;二次泛音或者三次谐波,其频率是基本频率的三倍,以此类推。谐波的相对振幅决定了信号波形的形状。
例如,一个方波可以表示为一系列正弦波的叠加,其中偶数谐波(即 2、4、6 等)的振幅是零,奇数谐波(1、3、5 等)的振幅比例是 1、1/3、1/5 等。对于锯齿形波来说,它含有所有的谐波,并且其振幅的比例是 1、1/2、1/3、1/4 等。
德国科学家赫尔姆霍茨(Hermann Helmholtz,1821--1894)认为这就是理解音色的关键所在。在她经典的 On the Sensations of Tone 一书中(1885 年出版,1954 年由 Dover Press 再版),赫尔姆霍茨假定耳朵和大脑或把复杂的音调分解为组成它们的正弦波,这些正弦波的相对强度就是我们感觉到的音色。不幸的是,后来证明事情并不是那么简单。
起音。当音符持续时,振幅保持恒定。这被称为延音。当音符结束时,振幅下跌到零,这被称为释音。
这些信号波形还会通过一个滤波器使得某些谐波衰减,这样就把简单的波形变成了一些更加复杂的、但在音乐上更有意思的声音。这些滤波器的截止频率也可以由一个包络线控制,以使声音的谐波内容在一个音符的过程中发生改变。
由于这些合成器是从处理含有大量谐波的波形开始,再使用滤波器衰减某些谐波,因此这种合成的方式就称为“减法合成” (subtractive synthesis)。
即使在使用减法合成的当时,许多从事电子音乐的人士就认为加法合成(additive synthesis)将称为将来的发展方向。
加法合成是从一定数量的正弦波发生器开始,每个发生器都调整在基本频率的整倍数上,以使这些正弦波对应于谐波。每一个谐波的振幅可以由包络线独立地控制。在加法合成中使用模拟电路是不实用的,因为会需要使用 8 到 24 个正弦波发生器来产生一个音符,而且这些正弦波发生器之间的相对频率必须精确地互相校正。而模拟信号波形发生器是出了名的不稳定而且容易产生频率漂移。
不过,对数字式合成器(可以通过查找表用数字方式生成波形)和由计算机生成的波形,频率漂移不再是问题,所以加法合成就变得可行了。大致的想法如 F:把一个真实的乐音记录下来,使用傅里叶分析把它变成一系列的谐波。这样可以确定每个谐波的相对强度,然后就可以使用多个正弦波来数字式地再生声音了。
译注:在声学中,物体振动时产生的每一个声音成分被称为分音,它用于描述构成复合声音的非整数倍振幅波形)。
人们发现,真实乐音的分音之间的不和谐性是使声音听起来真实的重要原因。严格的和谐性只会产生“电子”声音。每一个分音在一个音符的过程中在振幅和频率上都有变化。分音之间的相对频率和振幅关系在同一个乐器演奏不同的音高和强度时也不一样。真实乐音最复杂的部分发生在音符的起音部分,其中有非常大的不和谐性。人们发现音符的这个复杂的起音部分对人感知音色是非常重要的。
简而言之,真实乐器的声音比任何人想象的都复杂。通过分析乐音并且使用相对少量的简单包络线来控制分音的振幅和频率的想法显然是不实际的。
在 1977 年和 1978 年的早期 Computer Music Journal(当时由 People' Computer Company 出版,现在由 MIT Press 出版)上有对真实乐音的一些分析。James A. Moorer、John Grey 和 John Strawn 发表了三部系列文章 Lexicon of Analyzed Tones。它显示了在小提琴、双簧管、单簧管和小号上演奏一个音符(只有不到半秒的长度)的分音的振幅和频率图表。使用的音符是中音 C 调之上的降 E 音符。小提琴用了 20 个分音,双簧管和单簧管用了 21 个分音,小号用了 12 个分音。特别是,Computer Music Journal 在 1978 年 9 月第二卷第二期中还包含 3 对双簧管、单簧管和小号上各种各样的频率和振幅的包络线进行的线段模拟近似 (numerical line-segment approximations)。
而有了 Windows 的波形音频支持,我们就可以简单地把这些数字输入一个程序,为其中的每一个分音产生多个正弦波,然后把它们叠加起来,再把结果发送到波形音频卡上,从而重现 20 多年前录制的声音。ADDSYNETH (“additive synthesis”) 程序如图 22-7 所示。
/*--------------------------------------------------------
ADDSYNTH.C -- Additive Synthesis Sound Generation
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include <math.h>
#include "addsynth.h"
#include "resource.h"
#define ID_TIMER 1
#define SAMPLE_RATE 22050
#define MAX_PARTIALS 21
#define PI 3.14159
BOOL CALLBACK DlgProc(HWND, UINT, WPARAM, LPARAM);
TCHAR szAppName[] = TEXT("AddSynth");
// Sine wave generator
// --------------------
double SineGenerator(double dFreq, double * pdAngle)
{
double dAmp;
dAmp = sin(* pdAngle);
* pdAngle += 2 * PI * dFreq / SAMPLE_RATE;
if (*pdAngle >= 2 * PI)
* pdAngle -= 2 * PI;
return dAmp;
}
// Fill a buffer with composite waveform
// ------------------------------------
VOID FillBuffer(INS ins, PBYTE pBuffer, int iNumSamples)
{
static double dAngle[MAX_PARTIALS];
double dAmp, dFrq, dComp, dFrac;
int i, iPrt, iMsecTime, iCompMaxAmp, iMaxAmp, iSmp;
// Calculate the composite maximum amplitude
iCompMaxAmp = 0;
for (iPrt = 0; iPrt < ins.iNumPartials; iPrt++)
{
iMaxAmp = 0;
for (i = 0; i < ins.pprt[iPrt].iNumAmp; i++)
iMaxAmp = max(iMaxAmp, ins.pprt[iPrt].pEnvAmp[i].iValue);
iCompMaxAmp += iMaxAmp;
}
// Loop through each sample
for (iSmp = 0; iSmp < iNumSamples; iSmp++)
{
dComp = 0;
iMsecTime = (int)(1000 * iSmp / SAMPLE_RATE);
// Loop through each partial
for (iPrt = 0; iPrt < ins.iNumPartials; iPrt++)
{
dAmp = 0;
dFrq = 0;
for (i = 0; i < ins.pprt[iPrt].iNumAmp - 1; i++)
{
if (iMsecTime >= ins.pprt[iPrt].pEnvAmp[i].iTime &&
iMsecTime <= ins.pprt[iPrt].pEnvAmp[i + 1].iTime)
{
dFrac = (double)(iMsecTime -
ins.pprt[iPrt].pEnvAmp[i].iTime) /
(ins.pprt[iPrt].pEnvAmp[i + 1].iTime -
ins.pprt[iPrt].pEnvAmp[i].iTime);
dAmp = dFrac * ins.pprt[iPrt].pEnvAmp[i + 1].iValue +
(1 - dFrac) * ins.pprt[iPrt].pEnvAmp[i].iValue;
break;
}
}
for (i = 0; i < ins.pprt[iPrt].iNumFrq - 1; i++)
{
if (iMsecTime >= ins.pprt[iPrt].pEnvFrq[i].iTime &&
iMsecTime <= ins.pprt[iPrt].pEnvFrq[i + 1].iTime)
{
dFrac = (double)(iMsecTime -
ins.pprt[iPrt].pEnvFrq[i].iTime) /
(ins.pprt[iPrt].pEnvFrq[i + 1].iTime -
ins.pprt[iPrt].pEnvFrq[i].iTime);
dFrq = dFrac * ins.pprt[iPrt].pEnvFrq[i + 1].iValue +
(1 - dFrac) * ins.pprt[iPrt].pEnvFrq[i].iValue;
break;
}
}
dComp += dAmp * SineGenerator(dFrq, dAngle + iPrt);
}
pBuffer[iSmp] = (BYTE)(127 + 127 * dComp / iCompMaxAmp);
}
}
// Make a waveform file
// -------------------
BOOL MakeWaveFile(INS ins, TCHAR * szFileName)
{
DWORD dwWritten;
HANDLE hFile;
int iChunkSize, iPcmSize, iNumSamples;
PBYTE pBuffer;
WAVEFORMATEX waveform;
hFile = CreateFile(szFileName, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == NULL)
return FALSE;
iNumSamples = ((long)ins.iMsecTime * SAMPLE_RATE / 1000 + 1) / 2 * 2;
iPcmSize = sizeof(PCMWAVEFORMAT);
iChunkSize = 12 + iPcmSize + 8 + iNumSamples;
if (NULL == (pBuffer = (PBYTE)malloc(iNumSamples)))
{
CloseHandle(hFile);
return FALSE;
}
FillBuffer(ins, pBuffer, iNumSamples);
waveform.wFormatTag = WAVE_FORMAT_PCM;
waveform.nChannels = 1;
waveform.nSamplesPerSec = SAMPLE_RATE;
waveform.nAvgBytesPerSec = SAMPLE_RATE;
waveform.nBlockAlign = 1;
waveform.wBitsPerSample = 8;
waveform.cbSize = 0;
WriteFile(hFile, "RIFF", 4, &dwWritten, NULL);
WriteFile(hFile, &iChunkSize, 4, &dwWritten, NULL);
WriteFile(hFile, "WAVEfmt ", 8, &dwWritten, NULL);
WriteFile(hFile, &iPcmSize, 4, &dwWritten, NULL);
WriteFile(hFile, &waveform, sizeof(WAVEFORMATEX) - 2, &dwWritten, NULL);
WriteFile(hFile, "data", 4, &dwWritten, NULL);
WriteFile(hFile, &iNumSamples, 4, &dwWritten, NULL);
WriteFile(hFile, pBuffer, iNumSamples, &dwWritten, NULL);
CloseHandle(hFile);
free(pBuffer);
if ((int)dwWritten != iNumSamples)
{
DeleteFile(szFileName);
return FALSE;
}
return TRUE;
}
void TestAndCreateFile(HWND hwnd, INS ins, TCHAR * szFileName, int idButton)
{
TCHAR szMessage[64];
if (-1 != GetFileAttributes(szFileName))
EnableWindow(GetDlgItem(hwnd, idButton), TRUE);
else
{
if (MakeWaveFile(ins, szFileName))
EnableWindow(GetDlgItem(hwnd, idButton), TRUE);
else
{
wsprintf(szMessage, TEXT("Could not create %x."), szFileName);
MessageBeep(MB_ICONEXCLAMATION);
MessageBox(hwnd, szMessage, szAppName,
MB_OK | MB_ICONEXCLAMATION);
}
}
}
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;
}
BOOL CALLBACK DlgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static TCHAR * szTrum = TEXT("Trumpet.wav");
static TCHAR * szOboe = TEXT("Oboe.wav");
static TCHAR * szClar = TEXT("Clarinet.wav");
switch (message)
{
case WM_INITDIALOG:
SetTimer(hwnd, ID_TIMER, 1, NULL);
return TRUE;
case WM_TIMER:
KillTimer(hwnd, ID_TIMER);
SetCursor(LoadCursor(NULL, IDC_WAIT));
ShowCursor(TRUE);
TestAndCreateFile(hwnd, insTrum, szTrum, IDC_TRUMPET);
TestAndCreateFile(hwnd, insOboe, szOboe, IDC_OBOE);
TestAndCreateFile(hwnd, insClar, szClar, IDC_CLARINET);
SetDlgItemText(hwnd, IDC_TEXT, TEXT(" "));
SetFocus(GetDlgItem(hwnd, IDC_TRUMPET));
ShowCursor(FALSE);
SetCursor(LoadCursor(NULL, IDC_ARROW));
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_TRUMPET:
PlaySound(szTrum, NULL, SND_FILENAME | SND_SYNC);
return TRUE;
case IDC_OBOE:
PlaySound(szOboe, NULL, SND_FILENAME | SND_SYNC);
return TRUE;
case IDC_CLARINET:
PlaySound(szClar, NULL, SND_FILENAME | SND_SYNC);
return TRUE;
}
break;
case WM_SYSCOMMAND:
switch (LOWORD(wParam))
{
case SC_CLOSE:
EndDialog(hwnd, 0);
return TRUE;
}
break;
}
return FALSE;
}
ADDSYNTH.RC (excerpts)
// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"
/
//
// Dialog
//
ADDSYNTH DIALOG DISCARDABLE 100, 100, 176, 49
STYLE WS_MINIMIZEBOX | WS_CAPTION | WS_SYSMENU
CAPTION "Additive Synthesis"
FONT 8, "MS Sans Serif"
BEGIN
PUSHBUTTON "Trumpet", IDC_TRUMPET, 8, 8, 48, 16
PUSHBUTTON "Oboe", IDC_OBOE, 64, 8, 48, 16
PUSHBUTTON "Clarinet", IDC_CLARINET, 120, 8, 48, 16
LTEXT "Preparing Data...", IDC_TEXT, 8, 32, 100, 8
END
RESOURCE.H (excerpts)
// Microsoft Visual C++ generated include file.
// Used by AddSynth.rc
#define IDC_TRUMPET 1000
#define IDC_OBOE 1001
#define IDC_CLARINET 1002
#define IDC_TEXT 1003
包络线都被存储为一个 ENV 结构的数组。这些是成对的数值,第一个数值是以毫秒表示的时间,第二个数值是振幅(任意尺度)或者是频率(以每秒周期数为单位)。这些数组的长度是可变的,范围从 6~14。假设这些振幅和频率值之间以直线连接。
每种乐器由一系列的分音组成(小号用到了 12 个分音,双簧管和单簧管用到了 21 个分音),这些分音被存储为一个 PRT 结构的数组。PRT 结构中存放振幅和频率包络线的点的个数和指向 ENV 数组的指针。INS 结构存放以毫秒计的乐音的总时间、分音的数量和指向分音的 PRT 数组的指针。
ADDSYNTH 有三个按钮,分别标记为 “Trumpet” (小号),“Oboe”(双簧管) 和“Clarinet”(单簧管)。PC 计算机还没有足够快的速度实时完成所有加法合成的运算,因此,在你第一次运行 ADDSYNTH 程序时,这些按钮将是被禁用的,知道程序计算出样本并且创建了 TRUMPET.WAV、OBOE.WAVE 和 CLARINET.WAV 声音文件时,按钮才被启用。你可以用 PlaySound 函数播放这三个声音。当再次运行该程序时,它将首先检查这些波形文件是否已经存在,如果存在就不会再去创建文件了。
ADDSYNTH 的大多数工作是在 FillBuffer 函数中完成的。FillBuffer 首先计算累计最大振幅。它通过一个循环把乐器的所有分音的最大振幅找出来,然后把这些最大振幅全部加起来。这个数值在后面用于把采样按比例缩放到 8 位样本大小。
线性插值。
频率值与相位角值一起被传送给 SineGenerator 函数。正如我在本章前面讨论的,使用数字方法产生的正弦波要用到一个相位角。这个相位角要基于频率值持续递增。SineGenerator 函数返回的正弦值会和分音的振幅相乘并且累加起来。在样本的所有分音全部累加起来之后,样本被缩放到一个字节的大小。
22.2.11 波形音频闹钟
图 22-8 所示的 WAKEUP 看上去属于那种源代码文件不太完整地程序。程序的窗口看起来像一个对话框,但没有资源脚本(我们已经知道怎么做了)。程序似乎使用了一个波形文件,但是硬盘上却没有这样的文件。不过,这个程序相当有冲击力:它播放一个非常响亮和令人讨厌的声音。WAKEUP 是我的闹钟,它在把我吵醒这方面绝对管用。
/*--------------------------------------------------------
WAKEUP.C -- Alarm Clock Program
(c) Charles Petzold, 1998
--------------------------------------------------------*/
#include <Windows.h>
#include <CommCtrl.h>
// ID values for 3 child windows
#define ID_TIMEPICK 0
#define ID_CHECKBOX 1
#define ID_PUSHBTN 2
// Timer ID
#define ID_TIMER 1
// Number of 100-nanosecond increments (ie FILETIME ticks) in an hour
#define FTTICKSPERHOUR (60 * 60 * (LONGLONG) 10000000)
// Defines and structure fro waveform "file"
#define SAMPRATE 11025
#define NUMSAMPS (3 * SAMPRATE)
#define HALFSAMPS (NUMSAMPS / 2)
typedef struct
{
char chRiff[4];
DWORD dwRiffSize;
char chWave[4];
char chFmt[4];
DWORD dwFmtSize;
PCMWAVEFORMAT pwf;
char chData[4];
DWORD dwDataSize;
BYTE byData[0];
}
WAVEFORM;
// The window proc and the subclass proc
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
LRESULT CALLBACK SubProc(HWND, UINT, WPARAM, LPARAM);
// Original window procedure addresses for the subclassed windows
WNDPROC SubbedProc[3];
// The current child window with the input focus
HWND hwndFocus;
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInst,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("WakeUp");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.style = 0;
wndclass.lpfnWndProc = WndProc;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hInstance = hInstance;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)(1 + COLOR_BTNFACE);
wndclass.lpszMenuName = NULL;
wndclass.lpszClassName = szAppName;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("This program requires Windows NT!"),
szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, szAppName,
WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL);
ShowWindow(hwnd, iCmdShow);
UpdateWindow(hwnd);
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
static HWND hwndDTP, hwndCheck, hwndPush;
static WAVEFORM waveform = { "RIFF", NUMSAMPS + 0x24, "WAVE", "fmt ",
sizeof(PCMWAVEFORMAT), 1, 1, SAMPRATE,
SAMPRATE, 1, 8, "data", NUMSAMPS };
static WAVEFORM * pwaveform;
FILETIME ft;
HINSTANCE hInstance;
INITCOMMONCONTROLSEX icex;
int i, cxChar, cyChar;
LARGE_INTEGER li;
SYSTEMTIME st;
switch (message)
{
case WM_CREATE:
// Some initialization stuff
hInstance = (HINSTANCE)GetWindowLong(hwnd, GWL_HINSTANCE);
icex.dwSize = sizeof(icex);
icex.dwICC = ICC_DATE_CLASSES;
InitCommonControlsEx(&icex);
// Create the waveform file with alternating square waves
pwaveform = malloc(sizeof(WAVEFORM) + NUMSAMPS);
*pwaveform = waveform;
for (i = 0; i < HALFSAMPS; i++)
if (i % 600 < 300)
if (i % 16 < 8)
pwaveform->byData[i] = 25;
else
pwaveform->byData[i] = 2300;
else
if (i % 8 < 4)
pwaveform->byData[i] = 25;
else
pwaveform->byData[i] = 230;
// Get character size and set a fixed windwo size.
cxChar = LOWORD(GetDialogBaseUnits());
cyChar = HIWORD(GetDialogBaseUnits());
SetWindowPos(hwnd, NULL, 0, 0,
42 * cxChar,
10 * cyChar / 3 + 2 * GetSystemMetrics(SM_CYBORDER) +
GetSystemMetrics(SM_CYCAPTION),
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
// Create the three child windows
hwndDTP = CreateWindow(DATETIMEPICK_CLASS, TEXT(""),
WS_BORDER | WS_CHILD | WS_VISIBLE | DTS_TIMEFORMAT,
2 * cxChar, cyChar, 12 * cxChar, 4 * cyChar / 3,
hwnd, (HMENU)ID_TIMEPICK, hInstance, NULL);
hwndCheck = CreateWindow(TEXT("Button"), TEXT("Set Alarm"),
WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
16 * cxChar, cyChar, 12 * cxChar, 4 * cyChar / 3,
hwnd, (HMENU)ID_CHECKBOX, hInstance, NULL);
hwndPush = CreateWindow(TEXT("Button"), TEXT("Turn Off"),
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | WS_DISABLED,
28 * cxChar, cyChar, 12 * cxChar, 4 * cyChar / 3,
hwnd, (HMENU)ID_PUSHBTN, hInstance, NULL);
hwndFocus = hwndDTP;
// Subclass the three child windows
SubbedProc[ID_TIMEPICK] = (WNDPROC)
SetWindowLong(hwndDTP, GWL_WNDPROC, (LONG)SubProc);
SubbedProc[ID_CHECKBOX] = (WNDPROC)
SetWindowLong(hwndCheck, GWL_WNDPROC, (LONG)SubProc);
SubbedProc[ID_PUSHBTN] = (WNDPROC)
SetWindowLong(hwndPush, GWL_WNDPROC, (LONG)SubProc);
// Set the date and time picker control to the current time
// plus 9 hours, rounded down to next lowest hour
GetLocalTime(&st);
SystemTimeToFileTime(&st, &ft);
li = *(LARGE_INTEGER *)&ft;
li.QuadPart += 9 * FTTICKSPERHOUR;
ft = *(FILETIME *)&li;
FileTimeToSystemTime(&ft, &st);
st.wMinute = st.wSecond = st.wMilliseconds = 0;
SendMessage(hwndDTP, DTM_SETSYSTEMTIME, 0, (LPARAM)&st);
return 0;
case WM_SETFOCUS:
SetFocus(hwndFocus);
return 0;
case WM_COMMAND:
switch (LOWORD(wParam)) // control ID
{
case ID_CHECKBOX:
// When the user checks the "Set Alarm" button, get the
// time in the date and time control and subtract from
// it the current PC time.
if (SendMessage(hwndCheck, BM_GETCHECK, 0, 0))
{
SendMessage(hwndDTP, DTM_GETSYSTEMTIME, 0, (LPARAM)&st);
SystemTimeToFileTime(&st, &ft);
li = *(LARGE_INTEGER *)&ft;
GetLocalTime(&st);
SystemTimeToFileTime(&st, &ft);
li.QuadPart -= ((LARGE_INTEGER *)&ft)->QuadPart;
// Make sure the time is between 0 and 24 hours!
// These little adjustments let us completely ignore
// the date part of the SYSTEMTIME structures.
while (li.QuadPart < 0)
li.QuadPart += 24 * FTTICKSPERHOUR;
li.QuadPart %= 24 * FTTICKSPERHOUR;
// Set a one-shot timer! (See you in the morning.)
SetTimer(hwnd, ID_TIMER, (int)(li.QuadPart / 10000), 0);
}
// If button is being unchecked, kill the timer.
else
KillTimer(hwnd, ID_TIMER);
return 0;
// The "Turn Off" button turns off the ringing alarm, and also
// unchecks the "Set Alarm" button and disable itself.
case ID_PUSHBTN:
PlaySound(NULL, NULL, 0);
SendMessage(hwndCheck, BM_SETCHECK, 0, 0);
EnableWindow(hwndDTP, TRUE);
EnableWindow(hwndCheck, TRUE);
EnableWindow(hwndPush, FALSE);
SetFocus(hwndDTP);
return 0;
}
return 0;
// The WM_NOTIFY message comes from the date and time picker.
// If the user has checked "Set Alarm" and then gone back to
// change the alarm time, there might be a discrepancy between
// the displayed time and the one-shot timer. So, the program
// unchecks "Set Alarm" and kills any outstranding timer.
case WM_NOTIFY:
switch (wParam) // control ID
{
case ID_TIMEPICK:
switch (((NMHDR *) lParam)->code) // notification code
{
case DTN_DATETIMECHANGE:
if (SendMessage(hwndCheck, BM_GETCHECK, 0, 0))
{
KillTimer(hwnd, ID_TIMER);
SendMessage(hwndCheck, BM_SETCHECK, 0, 0);
}
return 0;
}
}
return 0;
// The WM_COMMAND message comes from the two buttons.
case WM_TIMER:
// When the timer message comes, kill the timer (because we only
// want a one-shot) and start the annoying alarm noise going.
KillTimer(hwnd, ID_TIMER);
PlaySound((PTSTR)pwaveform, NULL,
SND_MEMORY | SND_LOOP | SND_ASYNC);
// Let the sleepy user turn off the timer by slapping the
// space bar. If the window is minimized, it's restored; then
// it's brought to the forefront; then the pushbutton is enabled
// and given the input focus.
EnableWindow(hwndDTP, FALSE);
EnableWindow(hwndCheck, FALSE);
EnableWindow(hwndPush, TRUE);
hwndFocus = hwndPush;
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
return 0;
// Clean up if the alarm is ringing or the timer is still set.
case WM_DESTROY:
free(pwaveform);
if (IsWindowEnabled(hwndPush))
PlaySound(NULL, NULL, 0);
if (SendMessage(hwndCheck, BM_GETCHECK, 0, 0))
KillTimer(hwnd, ID_TIMER);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
LRESULT CALLBACK SubProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int idNext, id = GetWindowLong(hwnd, GWL_ID);
switch (message)
{
case WM_CHAR:
if (wParam == '\t')
{
idNext = id;
do
idNext = (idNext +
(GetKeyState(VK_SHIFT) < 0 ? 2 : 1)) % 3;
while (!IsWindowEnabled(GetDlgItem(GetParent(hwnd), idNext)));
SetFocus(GetDlgItem(GetParent(hwnd), idNext));
return 0;
}
break;
case WM_SETFOCUS:
hwndFocus = hwnd;
break;
}
return CallWindowProc(SubbedProc[id], hwnd, message, wParam, lParam);
}
WAKEUP 仅仅使用了两个方波,但它们被快速地交替使用。实际波形的计算发生在 WndProc 的 WM_CREATE 消息期间。整个波形文件存放在内存中。一个指向该内存区的指针被传送给 PlaySound 函数,该函数使用了 SND_MEMORY、SND_LOOP 和 SND_ASYNC 参数。
WAKEUP 使用了一个名为 DateTimePicker(日期时间选取器)的通用控件。该控件可以让用户选择一个特定的日期或时间。(WAKEUP 仅仅用到起选取时间的功能。)程序可以使用 SYSTEMTIME 结构来获取或设置这个时间,获取和设置 PC 始终也要用到这个结构。如果想看到 DateTimePicker 的功能到底有多强大,可以在创建窗口时不使用任何 DTS 样式标志。
请注意 WM_CREATE 消息最后的逻辑:程序假设,你在睡觉之前运行它,并且希望在 8 小时之后的下一个整点被叫醒。
显然,可以从 GetLocalTime 函数的 SYSTEMTIME 结构中得到当前的时间,然后“手工地”增加时间。但是一般来说,这样计算的话需要检查计算得到的小时数是否大于 24,如果是,那么你必须增加日期字段,之后也许还需要增加月份字段(因此你必须有每个月为几天和闰年计算的逻辑),而且你也许最后必须增加年份字段。
所以,推荐的方法(参见 //MSDN Library/WIN32 and COM development/System Services/Windows System Information/Time/Time Reference/Time Structures/SYSTEMTIME)是把 SYSTEMTIME 转换成 FILETIME 结构 (用 SystemTimeToFileTime 函数),再把 FILETIME 结构强制转换为 LARGE_INTEGER(大整数)结构,用大整数来进行运算,然后把结果强制转换回 FILETIME 结构,再转换回 SYSTEMTIME 结构(用 FileTimeToSystemTime 函数)。
FILETIME 结构,正如它的名称所示,是用来获取和设置文件的最后修改时间的。该结构如下所示:
type struct _FILETIME // ft
{
DWORD dwLowDateTime;
DWORD dwHighDateTime;
}
FILETIME;
这两个字段合在一起是一个 64 位数值,表示了从 1601 年 1 月 1 日起的,以 100 纳秒为单位的时间值。
Microsoft C/C++ 编译器支持 64 位整数,这是对 ANSI C 的一个非标准扩展。相应的数据类型是 __int64。可以对 __int64 类型进行所有正常的算术运算操作,并且一些运行时库函数也支持它们。Windows 的 WINNT.H 头文件有如下的定义:
typedef __int54 LONGLONG;
typedef unsigned __int64 DWORDLONG;
在 Windows 中,这个类型有时被称为四字(quad word),不过更常见的是称为大整数(large integer)。此外还定义了如下的联合:
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;
LONG HighPart;
};
LONGLONG QuadPart;
} LARGE_INTEGER;
相关文档可参见 //MSDN Library/WIN32 and COM Development/Development Guides/Windows API/Windows API Reference/Windows Data Type/Large Integers。该联合可以让你把一个大整数作为两个 32 位数或作为一个 64 位数来使用。