22.2 波形音频 I

摘录于《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  脉冲编码调制

        计算机以数字形式工作,所以为了把声音输入我们的计算机,有必要制定一种能将声音转化为数字,再把数字转化回声音的机制

        最常见的且不压缩数据的方法被称为“脉冲编码调制”(pulse code modulation,PCM)。PCM 被使用在 CD、数字音频磁带和 Windows 中。脉冲编码调制从概念上来说是一种十分简单的过程,但这个复杂的名字听上去比较好听。

        脉冲编码调制以固定的周期频率对波形进行采样,通常每秒几万次。对于每个样品,需要测量波形的幅值。模数转换器(analog-to-digital converter,ADC)负责将振幅转换为数字。同样,数模转换器(digital-to-analog,DAC)可将数字转换回电子波形。播放出来的声音和之前输入的声音并不完全相同,因为这样产生的波形在高频部分具有尖锐的边缘。处于这个原因,播放的硬件往往在数模转换器之后再连接一个低通滤波器。该滤波器能剔除高频率,产生平滑的波形。在输入端,低通滤波器被放置在 ADC 之前。

        脉冲编码调制有两个参数采样率(即每秒测量波形幅值的次数)和采样大小(即存储幅值水平的位数)。正如你可能已经猜到的那样,采样率越高、采样大小越大,就能越好地还原原始声音。然而,到某个时刻,任何对采样率和采样大小的改进都无法起作用了,因为已经超越了人类感知的分辨范围。另一方面,采样率和采样大小过低,可能会在还原音乐或其他声音的准确性方面产生问题。

22.2.3  采样率

        采样率决定了声音可被数字化并存储的最高频率。特别要指出的是,采样率必须两倍于采样声音的最高频率。这被称为“奈奎斯特频率”,是以 20 世纪 30 年代研究采样过程的工程师 Harry Nyquist 的名字命名的。

        如果使用过低的采样率对正弦波进行采样,产生的波形的频率会比原始版本低这就是所谓的走样。为了避免这个走样问题,应在输入端使用低通滤波器来阻止所有大于采样率一半的频率。在输出端,数模转换器所产生的波形的粗糙边缘实际上是大于采样率一半的频率组成的谐波。因此,输出端的低通滤波器也会阻止输出所有超过采样率一半的频率。

        音频 CD 使用的采样率是每秒 44100 个样品,即 44.1kHz。这以特定数字的由来如下。

        人耳能听到高达 20kHz 的声音,所以为了获取人类可以听到的整个音频范围,40kHz 的采样率是必要的。但是,由于低通滤波器有衰减效应,所以采样率应该高于这个值百分之十左右。现在,我们提高到了 44kHz。而为了应付我们可能同时录制数字音频与视频的情况,采样率应是美国和欧洲电视帧速率(分别是30Hz25Hz)的整数倍。这样就把采样率推高到了 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  采样大小

        脉冲编码调制的第二个参数是采样大小,单位是位(bit)。采样大小决定了可被录制和回放的最小声音和最大声音之间的差别。这就是所谓的动态范围

        声音强度是波形振幅(即每个正弦波在一个周期过程中可达到最大幅度的合成)的平方和频率相同,人类对声音强度的感觉是和波形振幅的对数成正比的

        两个声音强度之间的差异由贝尔(以电话的发明者亚历山大 ● 格雷厄姆 ● 贝尔命名)和分贝(dB)来衡量。1 贝尔表示声音强度增加 10 倍。1 分贝是 1 贝尔的十分之一(以等值乘法步骤计算)。因此,增加 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 递增的正弦波。

        众所周知,标准 C 运行时库包含一个 sin 函数,可以返回一个给定角度(以弧度形式给出)的正弦值。(两个 π 弧度等于 360 度。)sin 函数的返回值范围在-1~1之间。(我们在第 5 章的另一个名为 SINEWAVE 的程序中使用过此函数。)因此,利用 sin 函数可以很容易地生成正弦波数据,并把它输出到波形音频硬件上。基本上,只需将代表波形(在本例中就是正弦波)的数据填写到缓冲区,并将其传给 API。(实际上比这个要复杂一些,稍后我会提供更多细节。)当波形音频硬件播放完缓冲区内的内容后,你再将第二个缓冲区传给它,以此类推

        在第一次考虑这个问题(且不知道什么是 PCM)时,可能会认为将一个周期的正弦波分割为固定数量的样本是十分合理的(例如分割成 360 份)。对于 20Hz 的正弦波,这样每秒可以输出 7200 个样本。对于 200Hz 的正弦波,每秒会输出 72000 个样本。这可能可行,但并不是正确的做法。对于 5000Hz 的正弦波,这会需要每秒输出 1800 000 个样本,这必将使 DAC 承受很重的负担!此外,对于更高的频率,这种做法比所需要的精度高出了太多。

        在使用脉冲编码调制时,采样率是一个常数。让我们假设采样率是 11 025Hz(因为这时我在 SINEWAVE 程序中使用的采样率)。如果想要生成一个 2756.25Hz 的正弦波(正好是采样率的四分之一),则该正弦波的每个周期仅需采集 4 个样本。对于一个 25 Hz 的正弦波,每个周期则要求产生 441 个样本。一般来说每个周期的样本数量等于样本采样率除以所设计的正弦波的频率。一旦你知道了每个周期的样本数量,就可以用这个数字去除 2π 弧度,再使用 sin 函数来获取一个周期的样本。然后,只需不断重复这个周期的样本,就可以产生连续的波形。

        问题是,每个周期的样本数量很可能是小数,这样得到的波形在每个周期的末尾都不连续,所以这个方法也不行。

        使这个方法正确工作的关键是需要维持一个静态的“相位角” (phase angle)变量。这个角度初始值为 0.第一个样本是 0 度的正弦值。然后相位角递增,递增的值为频率的 2π 倍 除以采样率。此时使用该相位角产生第二个样本,并以这种方式继续下去。每当相位角超过 2π 弧度时,就从中减去 2π 弧度。但千万不要把相位角重新初始化为 0。

        例如,假设使用 11 025Hz 的采样率来生成一个 1000Hz 的正弦波。也就是说,每个周期大约有 11 个样品。相位角(为了更容易理解一些,在这里我以角度值而不是弧度值来表示它)在最初的大约一个半周期是 0、32.65、65.31、97.96、130.61、163.27、195.92、228.57、261.22、293.88、326.53、359.18、31.84、64.49、97.14、129.80、162.45、195.10 等。存入缓冲区的波形数据就是这些角度的正弦值,以每个样本的位数来表示。当为后续的缓冲区创建数据时,应继续递增最近的相位角的值,记住不要将其重新初始化为 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)类型的变量。在函数返回时,此变量的值就是在随后的波形输出调用中使用的句柄。

        waveOutOpen 的第二个参数是设备 ID。这使得该函数可以在安装有多块声卡的机器上使用。这个参数的范围可以从 0 至安装在该系统中的波形输出设备的数量减 1。可以调用 waveOutGetNumDevs 函数来获得波形输出设备的数量,调用 waveOutGetDevCaps 函数来找出其中每一项。如果想避免这种设备查询,可以使用常数 WAVE_MAPPER(定义为等于-1)来选择用户在控制面板的多媒体程序的【音频】选项卡的【首选设备】中指定的设备。如果当首选设备无法满足需求,而其他某个设备可用时,系统会自动选择另一个设备

        第三个参数是一个指向 WAVEFORMATEX 结构的指针。(稍后有更多关于此结构的信息。)第四个参数是一个窗口句柄,或是一个指向动态链接库中的回调函数的指针。这个参数指明了接收波形输出消息的窗口或回调函数。如果你使用回调函数,则可以在第五个参数中指定一些额外定义的数据。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 中指明,希望使用一个窗口过程来接收波形输出消息,所以waveOutOpen 函数会向程序的消息队列发送一条MM_WOM_OPEN 消息。该消息的wParam 参数被设为波形输出句柄。SINEWAVE 处理 MM_WOM_OPEN 消息,两次调用 FillBuffer 向 pBuffer 缓冲区填充正弦波数据。然后 SINEWAVE 将两个 WAVEHDR 结构传给waveOutWrite这个函数通过向波形输出硬件传输数据,真正开始播放声音

        当波形硬件播放完 waveOutWrite 函数传递给它的数据后,窗口将会收到一条 MM_WOM_DONE 消息。它的 wParam 参数是波形输出句柄,lParam 是一个指向 WAVEHDR 结构的指针。SINEWAVE 处理该消息,计算新的缓冲区的值,并调用 waveOutWrite 重新提交缓冲区。

        我们可以只用一个 WAVEHDR 结构和一个缓冲区来写 SINEWAVE 程序。然而,在波形硬件完成播放数据和程序处理 MM_WOM_DONE 消息提交一个新的缓冲区之间,会有短暂的时间延迟。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 函数以关闭该对话框并结束程序。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值