声音与音乐
WINDOWS和多媒体
从某种意义上来说,多媒体就是透过与装置无关的函式呼叫来获得对各种硬体的存取。让我们首先看一下硬体,然後再看看Windows多媒体API的结构。
多媒体硬体
或许最常用的多媒体硬体就是波形声音设备,也就是平常所说的音效卡。波形声音设备将麦克风的输入或其他声音输入转换为数位取样,并将其储存到记忆体或者储存到以.WAV为副档名的磁碟档案中。波形声音设备还将波形转换回类比声音,以便通过PC扩音器来播放。
音效卡通常还包含MIDI设备。MIDI是符合工业标准的乐器数位化介面(Musical Instrument Digital Interface)。这类硬体播放音符以回应短的二进位命令讯息。MIDI硬体通常还可以通过电缆连结到如音乐键盘等的MIDI输入设备上。通常,外部的MIDI合成器也能够添加到音效卡上。
现在,大多数PC上的CD-ROM驱动器都具备播放普通音乐CD的能力。这就是平常所说的「CD声音」。来自波形声音设备、MIDI设备以及CD声音设备的输出,一般在使用者的控制下用「音量控制」程式混合在一起。
另外几种普遍的多媒体「设备」不需要额外的硬体。Windows视讯设备(也称作AVI视讯设备)播放副档名为.AVI(audio-video interleave:声音视频插格)的电影或动画档案。「ActiveMovie控制项」可以播放其他型态的电影,包括QuickTime和MPEG。PC上的显示卡需要特定的硬体来协助播放这些电影。
还有个别PC使用者使用某种Pioneer雷射影碟机或者Sony VISCA系列录放影机。这些设备都有序列埠介面,因此可由PC软体来控制。某些显示卡具有一种称为「视窗影像(video in a window)」的功能,此功能允许一个外部的视讯信号与其他应用程式一起出现在Windows的萤幕上。这也可认为是一种多媒体设备。
API概述
在Windows中,API支援的多媒体功能主要分成两个集合。它们通常称为「低阶」和「高阶」介面。
低阶介面是一系列函式,这些函式以简短的说明性字首开头,而且在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions(与高阶函式一起)中列出。
低阶的波形声音输入输出函式的字首是waveIn和waveOut。我们将在本章看到这些函式。另外,本章还讨论用midiOut函式来控制MIDI输出设备。这些API还包括midiIn和midiStream函式。
本章还使用字首为time的函式,这些函式允许设定一个高解析度的计时器常式,其计时器的时间间隔速率最低能够到1毫秒。此程式主要用於播放MIDI音乐。其他几组函式包括声音压缩、视讯压缩以及动画和视讯序列,可惜的是本章不包括这些函式。
您还会注意到多媒体函式列表中七个带有字首mci的函式,它们允许存取媒体控制介面(MCI:Media Control Interface)。这是一个高阶的开放介面,用於控制多媒体PC中所有的多媒体硬体。MCI包括所有多媒体硬体都共有的许多命令,因为多媒体的许多方面都以磁带答录机这类设备播放/记录方式为模型。您为输入或输出而「打开」一台设备,进而可以「录音」(对於输入)或者「播放」(对於输出),并且结束後可以「关闭」设备。
MCI本身分为两种形式。一种形式下,可以向MCI发送讯息,这类似於Windows讯息。这些讯息包括位元编码标记和C资料结构。另一种形式下,可以向MCI发送文字字串。这个程式主要用於描述命令语言,此语言具有灵活的字串处理函式,但支援呼叫Windows API的函式不多。字串命令版的MCI还有利於交互研究和学习MCI,我们马上就举一个例子。MCI中的设备名称包括CD声音(cdaudio)、波形音响(waveaudio)、MIDI编曲器(sequencer)、影碟机(videodisc)、vcr、overlay(视窗中的类比视频)、dat(digital audio tape:数位式录频磁带)以及数位视频(digitalvideo)。MCI设备分为「简单型」和「混合型」。简单型设备(如CD声音)不使用档案。混合型设备(如波形音响)则使用档案。使用波形音响时,这些档案的副档名是.WAV。
存取多媒体硬体的另一种方法包括DirectX API,它超出了本书的范围。
另外两个高阶多媒体函式也值得一提:MessageBeep和PlaySound,它们在第三章有示范。MessageBeep播放「控制台」的「声音」中指定的声音。PlaySound可播放磁碟上、记忆体中或者作为资源载入的.WAV档案。本章的後面还会用到PlaySound函式。
用TESTMCI研究MCI
在Windows多媒体的早期,软体开发套件含有一个名为MCITEST的C程式,它允许程式写作者交谈式输入MCI命令并学习这些命令的工作方式。这个程式,至少是C语言版,显然已经消失了。因此,我又重新建立了它,即程式22-1所示的TESTMCI程式。虽然我不认为目前程式码与旧的程式码有什么区别,但现在的使用者介面还是依据以前的MCITEST程式,并且没有使用现在的程式码。
程式22-1 TESTMCI TESTMCI.C /*--------------------------------------------------------------------------- TESTMCI.C -- MCI Command String Tester (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_TIMER 1 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("TestMci") ; 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 HWND hwndEdit ; int iCharBeg, iCharEnd, iLineBeg, iLineEnd, iChar, iLine, iLength ; MCIERROR error ; RECT rect ; TCHAR szCommand [1024], szReturn [1024], szError [1024], szBuffer [32] ; switch (message) { case WM_INITDIALOG: // Center the window on screen GetWindowRect (hwnd, &rect) ; SetWindowPos (hwnd, NULL, (GetSystemMetrics (SM_CXSCREEN) - rect.right + rect.left) / 2, (GetSystemMetrics (SM_CYSCREEN) - rect.bottom + rect.top) / 2, 0, 0, SWP_NOZORDER | SWP_NOSIZE) ; hwndEdit = GetDlgItem (hwnd, IDC_MAIN_EDIT) ; SetFocus (hwndEdit) ; return FALSE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDOK: // Find the line numbers corresponding to the selection SendMessage (hwndEdit, EM_GETSEL, (WPARAM) &iCharBeg, (LPARAM) &iCharEnd) ; iLineBeg = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharBeg, 0) ; iLineEnd = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharEnd, 0) ; // Loop through all the lines for (iLine = iLineBeg ; iLine <= iLineEnd ; iLine++) { // Get the line and terminate it; ignore if blank * (WORD *) szCommand = sizeof (szCommand) / sizeof (TCHAR) ; iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, (LPARAM) szCommand) ; szCommand [iLength] = '\0' ; if (iLength == 0) continue ; // Send the MCI command error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ; // Set the Return String field SetDlgItemText (hwnd, IDC_RETURN_STRING, szReturn) ; // Set the Error String field (even if no error) mciGetErrorString (error, szError, sizeof (szError) / sizeof (TCHAR)) ; SetDlgItemText (hwnd, IDC_ERROR_STRING, szError) ; } // Send the caret to the end of the last selected line iChar = SendMessage (hwndEdit, EM_LINEINDEX, iLineEnd, 0) ; iChar += SendMessage (hwndEdit, EM_LINELENGTH, iCharEnd, 0) ; SendMessage (hwndEdit, EM_SETSEL, iChar, iChar) ; // Insert a carriage return/line feed combination SendMessage (hwndEdit, EM_REPLACESEL, FALSE, (LPARAM) TEXT ("\r\n")) ; SetFocus (hwndEdit) ; return TRUE ; case IDCANCEL: EndDialog (hwnd, 0) ; return TRUE ; case IDC_MAIN_EDIT: if (HIWORD (wParam) == EN_ERRSPACE) { MessageBox (hwnd, TEXT ("Error control out of space."), szAppName, MB_OK | MB_ICONINFORMATION) ; return TRUE ; } break ; } break ; case MM_MCINOTIFY: EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), TRUE) ; wsprintf (szBuffer, TEXT ("Device ID = %i"), lParam) ; SetDlgItemText (hwnd, IDC_NOTIFY_ID, szBuffer) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), wParam & MCI_NOTIFY_SUCCESSFUL) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), wParam & MCI_NOTIFY_SUPERSEDED) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), wParam & MCI_NOTIFY_ABORTED) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), wParam & MCI_NOTIFY_FAILURE) ; SetTimer (hwnd, ID_TIMER, 5000, NULL) ; return TRUE ; case WM_TIMER: KillTimer (hwnd, ID_TIMER) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), FALSE) ; return TRUE ; case WM_SYSCOMMAND: switch (LOWORD (wParam)) { case SC_CLOSE: EndDialog (hwnd, 0) ; return TRUE ; } break ; } return FALSE ; }
TESTMCI.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog TESTMCI DIALOG DISCARDABLE 0, 0, 270, 276 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "MCI Tester" FONT 8, "MS Sans Serif" BEGIN EDITTEXT IDC_MAIN_EDIT,8,8,254,100,ES_MULTILINE | ES_AUTOHSCROLL | WS_VSCROLL LTEXT "Return String:",IDC_STATIC,8,114,60,8 EDITTEXT IDC_RETURN_STRING,8,126,120,50,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_GROUP | NOT WS_TABSTOP LTEXT "Error String:",IDC_STATIC,142,114,60,8 EDITTEXT IDC_ERROR_STRING,142,126,120,50,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | NOT WS_TABSTOP GROUPBOX "MM_MCINOTIFY Message",IDC_STATIC,9,186,254,58 LTEXT "",IDC_NOTIFY_ID,26,198,100,8 LTEXT "MCI_NOTIFY_SUCCESSFUL",IDC_NOTIFY_SUCCESSFUL,26,212,100, 8,WS_DISABLED LTEXT "MCI_NOTIFY_SUPERSEDED",IDC_NOTIFY_SUPERSEDED,26,226,100, 8,WS_DISABLED LTEXT "MCI_NOTIFY_ABORTED",IDC_NOTIFY_ABORTED,144,212,100,8, WS_DISABLED LTEXT "MCI_NOTIFY_FAILURE",IDC_NOTIFY_FAILURE,144,226,100,8, WS_DISABLED DEFPUSHBUTTON "OK",IDOK,57,255,50,14 PUSHBUTTON "Close",IDCANCEL,162,255,50,14 END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by TestMci.rc #define IDC_MAIN_EDIT 1000 #define IDC_NOTIFY_MESSAGE 1005 #define IDC_NOTIFY_ID 1006 #define IDC_NOTIFY_SUCCESSFUL 1007 #define IDC_NOTIFY_SUPERSEDED 1008 #define IDC_NOTIFY_ABORTED 1009 #define IDC_NOTIFY_FAILURE 1010 #define IDC_SIGNAL_MESSAGE 1011 #define IDC_SIGNAL_ID 1012 #define IDC_SIGNAL_PARAM 1013 #define IDC_RETURN_STRING 1014 #define IDC_ERROR_STRING 1015 #define IDC_DEVICES 1016 #define IDC_STATIC -1
与本章的大多数程式一样,TESTMCI使用非模态对话方块作为它的主视窗。与本章所有的程式一样,TESTMCI要求WINMM.LIB引用程式库在Microsoft Visual C++「Projects Settings」对话方块的「Links」页列出。
此程式用到了两个最重要的多媒体函式:mciSendString和mciGetErrorText。在TESTMCI的主编辑视窗输入一些内容然後按下Enter键(或「OK」按钮)後,程式将输入的字串作为第一个参数传递给mciSendString命令:
error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ;
如果在编辑视窗选择了不止一行,则程式将按顺序将它们发送给mciSendString函式。第二个参数是字串位址,此字串取得从函式传回的资讯。程式将此资讯显示在视窗的「Return String」区域。从mciSendString传回的错误代码传递给mciGetErrorString函式,以获得文字错误说明;此说明显示在TESTMCI视窗的「Error String」区域。
MCITEXT和CD声音
通过控制CD-ROM驱动器和播放声音CD,您会对MCI命令字串留下很好的印象。因为这些命令字串一般都非常简单,并且更重要的是您可以听到一些音乐,所以这是好的起点。您可以在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Command Strings中获得MCI命令字串的参考,以方便本练习。
请确认CD-ROM驱动器的声音输出已连结到扩音器或耳机,然後放入一张声音CD,如Bruce Springsteen的「Born to Run」。Windows 98中,「CD播放程式」将启动并开始播放此唱片。如果是这样的话,终止「CD播放程式」,然後可以叫出TESTMCI并且键入命令:
open cdaudio
然後按Enter键。其中open是MCI命令,cdaudio是MCI认定的CD-ROM驱动器的设备名称(假定您的系统中只有一个CD-ROM驱动器。要获得多个CD-ROM驱动器名称需使用sysinfo命令)。
TESTMCI中的「Return String」区域显示mciSendString函式中系统传回给程式的字串。如果执行了open命令,则此值是1。TESTMCI在「Error String」区域中显示mciGetErrorString依据mciSendString传回值所传回的资讯。如果mciSendString没有传回错误代码,则「Error String」区域显示文字"The specified command was carried out"。
假定执行了open命令,现在就可以输入:
play cdaudio
CD将开始播放唱片上的第一首乐曲「Thunder Road」。输入下面的命令可以暂停播放:
pause cdaudio
或者
stop cdaudio
对於CD声音设备来说,这些叙述的功能相同。您可用下面的叙述重新播放:
play cdaudio
迄今为止,我们使用的全部字串都由命令和设备名称组成。其中有些命令带有选项。例如,键入:
status cdaudio position
根据收听时间的长短,「Return String」区域将显示类似下面的一些字元:
01:15:25
这是些什么?很显然不是小时、分钟和秒,因为CD没有那么长。要找出时间格式,请键入:
status cdaudio time format
现在「Return String」区域显示下面的字串:
msf
这代表「分-秒-格」。CD声音中,每秒有75格。时间格式的讯格部分可在0到74之间的范围内变化。
状态命令有一连串的选项。使用下面的命令,您可以确定msf格式的CD全部长度:
status cdaudio length
对於「Born to Run」,「Return String」区域将显示:
39:28:19
这指的是39分28秒19格。
现在试一下
status cdaudio number of tracks
「Return String」区域将显示:
8
我们从CD封面上知道「Born to Run」CD上第五首乐曲是主题曲。MCI命令中的乐曲从1开始编号。要想知道乐曲「Born to Run」的长度,可以键入下面的命令:
status cdaudio length track 5
「Return String」区域将显示:
04:30:22
我们还可确定此乐曲从盘上的哪个位置开始:
status cdaudio position track 5
「Return String」区域将显示:
17:36:35
根据这条资讯,我们可以直接跳到乐曲标题:
play cdaudio from 17:36:35 to 22:06:57
此命令只播放一首乐曲,然後停止。最後的值是由4:30:22(乐曲长度)加17:36:35得到的。或者,也可以用下面的命令确定:
status cdaudio position track 6
或者,也可以将时间格式设定为乐曲-分-秒-格:
set cdaudio time format tmsf
然後
play cdaudio from 5:0:0:0 to 6:0:0:0
或者,更简单地
play cdaudio from 5 to 6
如果时间的尾部是0,那么您可去掉它们。还可以用毫秒设定时间格式。
每个MCI命令字串都可以在字串的後面包括选项wait和notify(但不是同时使用)。例如,假设您只想播放「Born to Run」的前10秒,而且播放後,您还想让程式完成其他工作。您可按下面的方法进行(假定您已经将时间格式设定为tmsf):
play cdaudio from 5:0:0 to 5:0:10 wait
这种情况下,直到函式执行结束,也就是说,直到播放完「Born to Run」的前10秒,mciSendString函式才传回。
现在很明显,一般来说,在单执行绪的应用程式中这不是一件好事。如果不小心键入:
play cdaudio wait
直到整个唱片播放完以後,mciSendString函式才将控制权传回给程式。如果必须使用wait选项(在只要执行MCI描述档案而不管其他事情的时候,这么做很方便,与我将展示的一样),首先使用break命令。此命令可设定一个虚拟键码,此码将中断mciSendString命令并将控制权传回给程式。例如,要设定Escape键来实作此目的,可用:
break cdaudio on 27
这里,27是十进位的VK_ESCAPE值。
比wait选项更好的是notify选项:
play cdaudio from 5:0:0 to 5:0:10 notify
这种情况下,mciSendString函式立即传回,但如果该操作在MCI命令的尾部定义,则mciSendString函式的最後一个参数所指定代号的视窗会收到MM_MCINOTIFY讯息。TESTMCI程式在MM_MCINOTIFY框中显示此讯息的结果。为避免与其他可能键入的命令混淆,TESTMCI程式在5秒後停止显示MM_MCINOTIFY讯息的结果。
您可以同时使用wait和notify关键字,但没有理由这么做。不使用这两个关键字,内定的操作就既不是wait,也不是您通常所希望的notify。
用这些命令结束播放时,可键入下面的命令来停止CD:
stop cdaudio
如果在关闭之前没有停止CD-ROM设备,那么甚至在关闭设备之後还会继续播放CD。
另外,您还可以试试您的硬体允许或者不允许的一些命令:
eject cdaudio
最後按下面的方法关闭设备:
close cdaudio
虽然TESTMCI自己不能储存或载入文字档案,但可以在编辑控制项和剪贴簿之间复制文字:先从TESTMCI选择一些内容,将其复制到剪贴簿(用Ctrl-C),再将这些文字从剪贴簿复制到「记事本」,然後储存。相反的操作,可以将一系列的MCI命令载入到TESTMCI。如果选择了一系列命令然後按下「OK」按钮(或者Enter键),则TESTMCI将每次执行一条命令。这就允许您编写MCI的「描述档案」,即MCI命令的简单列表。
例如,假设您想听歌曲「Jungleland」(唱片中的最後一首)、「Thunder Road」和「Born to Run」,并要按此顺序听,可以编写如下的描述命令:
open cdaudio set cdaudio time format tmsf break cdaudio on 27 play cdaudio from 8 wait play cdaudio from 1 to 2 wait play cdaudio from 5 to 6 wait stop cdaudio eject cdaudio close cdaudio
不用wait关键字,就不能正常工作,因为mciSendString命令会立即传回,然後执行下一条命令。
此时,如何编写模拟CD播放程式的简单应用程式,就应该相当清楚了。程式可以确定乐曲数量、每个乐曲的长度并能显示允许使用者从任意位置开始播放(不过,请记住:mciSendString总是传回文字字串资讯,因此您需要编写解析处理程式来将这些字串转换成数字)。可以肯定,这样的程式还要使用Windows计时器,以产生大约1秒的时间间隔。在WM_TIMER讯息处理期间,程式将呼叫:
status cdaudio mode
来查看CD是暂停还是在播放。
status cdaudio position
命令允许程式更新显示以给使用者显示目前的位置。但可能还存在更令人感兴趣的事:如果程式知道音乐音调部分的节拍位置,那么就可以使萤幕上的图形与CD同步。这对於音乐指令或者建立自己的图形音乐视讯程式极为有用。
波形声音
波形声音是最常用的Windows多媒体特性。波形声音设备可以通过麦克风捕捉声音,并将其转换为数值,然後把它们储存到记忆体或者磁碟上的波形档案中,波形档案的副档名是.WAV。这样,声音就可以播放了。
声音与波形
在接触波形声音API之前,具备一些预备知识很重要,这些知识包括物理学、听觉以及声音进出电脑的程序。
声音就是振动。当声音改变了鼓膜上空气的压力时,我们就感觉到了声音。麦克风可以感应这些振动,并且将它们转换为电流。同样,电流再经过放大器和扩音器,就又变成了声音。传统上,声音以类比方式储存(例如录音磁带和唱片),这些振动储存在磁气脉冲或者轮廓凹槽中。当声音转换为电流时,就可以用随时间振动的波形来表示。振动最自然的形式可以用正弦波表示,它的一个周期如图5-5所示。
正弦波有两个参数-振幅(也就是一个周期中的最大振幅)和频率。我们已知振幅就是音量,频率就是音调。一般来说人耳可感受的正弦波的范围是从20Hz(每秒周期)的低频声音到20,000Hz的高频声,但随著年龄的增长,对高频声音的感受能力会逐年退化。
人感受频率的能力与频率是对数关系而不是线性关系。也就是说,我们感受20Hz到40Hz的频率变化与感受40Hz到80Hz的频率变化是一样的。在音乐中,这种加倍的频率定义为八度音阶。因此,人耳可感觉到大约10个八度音阶的声音。钢琴的范围是从27.5 Hz到4186 Hz之间,略小於7个八度音阶。
虽然正弦波代表了振动的大多数自然形式,但纯正弦波很少在现实生活中单独出现,而且,纯正弦波并不动听。大多数声音都很复杂。
任何周期的波形(即,一个回圈波形)可以分解成多个正弦波,这些正弦波的频率都是整倍数。这就是所谓的Fourier级数,它以法国数学家和物理学家Jean Baptiste Joseph Fourier(1768-1830)的名字命名。周期的频率是基础。级数中其他正弦波的频率是基础频率的2倍、3倍、4倍(等等)。这些频率的声音称为泛音。基础频率也称作一级谐波。第一泛音是二级谐波,以此类推。
正弦波谐波的相对强度给每个周期的波形唯一的声音。这就是「音质」,它使得喇叭吹出喇叭声,钢琴弹出钢琴声。
人们一度认为电子合成乐器仅仅需要将声音分解成谐波并且与多个正弦波重组即可。不过,事实证明现实世界中的声音并不是这么简单。代表现实世界中声音的波形都没有严格的周期。乐器之间谐波的相对强度是不同的,并且谐波也随著每个音符的演奏时间改变。特别是乐器演奏音符的开始位置-我们称作起奏(attack)-相当复杂,但这个位置又对我们感受音质至关重要。
由於近年来数位储存能力的提高,我们可以将声音直接以数位形式储存而不用复杂的重组。
脉冲编码调制(Pulse Code Modulation)
电脑处理的是数值,因此要使声音进入电脑,就必须设计一种能将声音与数位信号相互转换的机制。
不压缩资料就完成此功能的最常用方法称作「脉冲编码调制」(PCM:pulse code modulation)。PCM可用在光碟、数位式录音磁带以及Windows中。脉冲编码调制其实只是一种概念上很简单的处理步骤的奇怪代名词而已。
利用脉冲编码调制,波形可以按固定的周期频率取样,其频率通常是每秒几万次。对於每个样本都测量其波形的振幅。完成将振幅转换成数位信号工作的硬体是类比数位转换器(ADC:analog-to-digital converter)。类似地,通过数位类比转换器(DAC:digital-to-analog converter)可将数位信号转换回波形电子信号。但这样转换得到的波形与输入的并不完全相同。合成的波形具有由高频组成的尖锐边缘。因此,播放硬体通常在数位类比转换器後还包括一个低通滤波器。此滤波器滤掉高频,并使合成後的波形更平滑。在输入端,低通滤波器位於ADC前面。
脉冲编码调制有两个参数:取样频率,即每秒内测量波形振幅的次数;样本大小,即用於储存振幅级的位元数。与您想像的一样:取样频率越高,样本大小越大,原始声音的复制品才更好。不过,存在一个提高取样频率和样本大小的极点,超过这个极点也就超过了人类分辨声音的极限。另外,如果取样频率和样本大小过低,将导致不能精确地复制音乐以及其他声音。
取样频率
取样频率决定声音可被数位化和储存的最大频率。尤其是,取样频率必须是样本声音最高频率的两倍。这就是「Nyquist频率(Nyquist Frequency)」,以30年代研究取样程序的工程师Harry Nyquist的名字命名。
以过低的取样频率对正弦波取样时,合成的波形比最初的波形频率更低。这就是所说的失真信号。为避免失真信号的发生,在输入端使用低通滤波器以阻止频率大於半个取样频率的所有波形。在输出端,数位类比转换器产生的粗糙的波形边缘实际上是由频率大於半个取样频率的波形组成的泛音。因此,位於输出端的低通滤波器也阻止频率大於半个取样频率的所有波形。
声音CD中使用的取样频率是每秒44,100个样本,或者称为44.1kHz。这个特有的数值是这样产生的:
人耳可听到最高20kHz的声音,因此要拦截人能听到的整个声音范围,就需要40kHz的取样频率。然而,由於低通滤波器具有频率下滑效应,所以取样频率应该再高出大约百分之十才行。现在,取样频率就达到了44kHz。这时,我们要与视讯同时记录数位声音,於是取样频率就应该是美国、欧洲电视显示格速率的整数倍,这两种视讯格速率分别是30Hz和25Hz。这就使取样频率升高到了44.1kHz。
取样频率为44.1kHz的光碟会产生大量的资料,这对於一些应用程式来说实在是太多了,例如对於录制声音而不是录制音乐时就是这样。把取样频率减半到22.05 kHz,可由一个10 kHz的泛音来简化复制声音的上半部分。再将其减半到11.025 kHz就向我们提供了5 kHz频率范围。44.1 kHz、22.05 kHz和11.025 kHz的取样频率,以及8 kHz都是波形声音设备普遍支援的标准。
因为钢琴的最高频率为4186 Hz,所以您可能会认为给钢琴录音时,11.025 kHz的取样频率就足够了。但4186 Hz只是钢琴最高的基础频率而已,滤掉大於5000Hz的所有正弦波将减少可被复制的泛音,而这样将不能精确地捕捉和复制钢琴的声音。
样本大小
脉冲编码调制的第二个参数是按位元计算的样本大小。样本大小决定了可供录制和播放的最低音与最高音之间的区别。这就是通常所说的动态范围。
声音强度是波形振幅的平方(即每个正弦波一个周期中最大振幅的合成)。与频率一样,人对声音强度的感受也呈对数变化。
两个声音在强度上的区别是以贝尔(以电话发明人Alexander Graham Bell的名字命名)和分贝(dB)为单位进行测量的。1贝尔在声音强度上呈10倍增加。1dB就是以相同的乘法步骤成为1贝尔的十分之一。由此,1dB可增加声音强度的1.26倍(10的10次方根),或者增加波形振幅的1.12倍(10的20次方根)。1分贝是耳朵可感觉出的声强的最小变化。从开始能听到的声音极限到让人感到疼痛的声音极限之间的声强差大约是100 dB。
可用下面的公式来计算两个声音间的动态范围,单位是分贝:
其中A1和A2是两个声音的振幅。因为只可能有一个振幅,所以样本大小是1位元,动态范围是0。
如果样本大小是8位元,则最大振幅与最小振幅之间的比例就是256。这样,动态范围就是:
或者48分贝。48的动态范围大约相当於非常安静的房屋与电动割草机之间的差别。将样本大小加倍到16位元产生的动态范围是:
或者96分贝。这非常接近听觉极限和疼痛极限,而且人们认为这就是复制音乐的理想值。
Windows同时支援8位元和16位元的样本大小。储存8位元的样本时,样本以无正负号位元组处理,静音将储存为一个值为0x80的字串。16位元的样本以带正负号整数处理,这时静音将储存为一个值为0的字串。
要计算未压缩声音所需的储存空间,可用以秒为单位的声音持续时间乘以取样频率。如果用16位元样本而不是8位元样本,则将其加倍,如果是录制立体声则再加倍。例如,1小时的CD声音(或者是在每个立体声样本占2位元组、每秒44 ,100个样本的速度下进行3 600秒)需要635MB,这快要接近一张CD-ROM的储存量了。
在软体中产生正弦波
对於第一个关於波形声音的练习,我们不打算将声音储存到档案中或播放录制的声音。我们将使用低阶的波形声音API(即,字首是waveOut的函式)来建立一个称作SINEWAVE的声音正弦波生成器。此程式以1 Hz的增量来生成从20Hz(人可感觉的最低值)到5,000Hz(与人感觉的最高值相差两个八度音阶)的正弦波。
我们知道,标准C执行时期程式库包括了一个sin函式,该函式传回一个弧度角的正弦值(2π弧度等於360度)。sin函式传回值的范围是从-1到1(早在第五章,我们就在SINEWAVE程式中使用过这个函式)。因此,应该很容易使用sin函式生成输出到波形声音硬体的正弦波资料。基本上是用代表波形(这时是正弦波)的资料来填充缓冲区,并将此缓冲区传递给API。(这比前面所讲的稍微有些复杂,但我将详细介绍)。波形声音硬体播放完缓冲区中的资料後,应将第二个缓冲区中的资料传递给它,并且以此类推。
第一次考虑这个问题(而且对PCM也一无所知)时,您大概会认为将一个周期的正弦波分成若干固定数量的样本-例如360个-才合理。对於20 Hz的正弦波,每秒输出7,200个样本。对於200 Hz的正弦波,每秒则要输出72,000个样本。这有可能实作,但实际上却不能这么做。对於5,000 Hz的正弦波,就需要每秒输出1,800,000个样本,这的确会增大DAC的负担!更重要的是,对於更高的频率,这种作法会比实际需要的精确度还高。
就脉冲编码调制而言,取样频率是个常数。假定取样频率是SINEWAVE程式中使用的11,025Hz。如果要生成一个2,756.25Hz(确切地说是四分之一的取样频率)的正弦波,则正弦波的每个周期就有4个样本。对於25Hz的正弦波,每个周期就有441个样本。通常,每周期的样本数等於取样频率除以要得到的正弦波频率。一旦知道了每周期的样本数,用2π弧度除以此数,然後用sin函式来获得每周期的样本。然後再反复对一个周期进行取样,从而建立一个连续的波形。
问题是每周期的样本数可能带有小数,因此在使用时这种方法并不是很好。每个周期的尾部都会有间断。
使它正常工作的关键是保留一个静态的「相位角」变数。此角初始化为0。第一个样本是0度正弦。随後,相位角增加一个值,该值等於2π乘以频率再除以取样频率。用此相位角作为第二个样本,并且按此方法继续。一旦相位角超过2π弧度,则减去2π弧度,而不要把相位角再初始化为0。
例如,假定要用11,025Hz的取样频率来生成1,000Hz的正弦波。即每周期有大约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所示,FillBuffer函式完成这项工作-与SINEWAVE程式的其余部分一起完成。
程式22-2 SINEWAVE SINEWAVE.C /*------------------------------------------------------------------------- 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 = malloc (sizeof (WAVEHDR)) ; pWaveHdr2 = malloc (sizeof (WAVEHDR)) ; pBuffer1 = malloc (OUT_BUFFER_SIZE) ; pBuffer2 = 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 = 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 = 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 (((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 (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.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 (摘录) // Microsoft Developer Studio 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」的按钮。按下此按钮後,您将从连结音效卡的扩音器中听到正弦波的声音,同时按钮上的文字将变成「Turn Off」。用键盘或者滑鼠移动卷动列可以改变频率。要关闭声音,可以再次按下按钮。
SINEWAVE程式码初始化卷动列,以便频率在WM_INITDIALOG讯息处理期间最低是20Hz,最高是5000Hz。初始化时,卷动列设定为440 Hz。用音乐术语来说就是中音上面的A,它在管弦乐队演奏时用来调音。DlgProc在接收WM_HSCROLL讯息处理期间改变静态变数iFreq。注意,Page Left和Page Right将导致DlgProc增加或者减少一个八度音阶。
当DlgProc从按钮收到一个WM_COMMAND讯息时,它首先配置4个记忆体块-2个用於WAVEHDR结构,我们马上讨论。另两个用於缓冲区储存波形资料,我们将这两个缓冲区称为pBuffer1和pBuffer2。
通过呼叫waveOutOpen函式,SINEWAVE打开波形声音设备以便输出,waveOutOpen函式使用下面的参数:
waveOutOpen (&hWaveOut, wDeviceID, &waveformat, dwCallBack, dwCallBackData, dwFlags) ;
将第一个参数设定为指向HWAVEOUT(handle to waveform audio output:波形声音输出代号)型态的变数。从函式传回时,此变数将设定为一个代号,後面的波形输出呼叫中将使用该代号。
waveOutOpen的第二个参数是设备ID。它允许函式可以在安装多个音效卡的机器上使用。参数的范围在0到系统所安装的波形输出设备数之间。呼叫waveOutGetNumDevs可以获得波形输出设备数,而呼叫waveOutGetDevCaps可以找出每个波形输出设备。如果想消除设备问号,那么您可以用常数WAVE_MAPPER(定义为-1)来选择设备,该设备在「控制台」的「多媒体」中「音效」页面标签里的「喜欢使用的装置」中指定。另外,如果首选设备不能满足您的需要,而其他设备可以,那么系统将选择其他设备。
第三个参数是指向WAVEFORMATEX结构的指标(後面将详细介绍)。第四个参数是视窗代号或指向动态连结程式库中callback函式的指标,用来表示接收波形输出讯息的视窗或者callback函式。使用cal
评论