声音与音乐
在Microsoft Windows中,声音、音乐与视讯的综合运用是一个重要的进步。对多媒体的支持起源于1991年所谓的Microsoft Windows多媒体延伸功能(Multimedia Extensions to Microsoft Windows)。1992年,Windows 3.1的发布使得对多媒体的支持成为另一类API。最近几年,CD-ROM驱动器和声卡-在90年代初期还很少见-已成为新PC的标准配备。现在,几乎所有的人们都深信: 多媒体在很大程度上有益于Windows的可视化图形,从而使计算机摆脱了其只是处理数字和文字的机器的传统角色。
WINDOWS和多媒体
从某种意义上来说,多媒体就是透过与设备无关的函数呼叫来获得对各种硬件的存取。让我们首先看一下硬件,然后再看看Windows多媒体API的结构。
多媒体硬件
或许最常用的多媒体硬件就是波形声音设备,也就是平常所说的声卡。波形声音设备将麦克风的输入或其它声音输入转换为数字取样,并将其储存到内存或者储存到以.WAV为扩展名的磁盘文件中。波形声音设备还将波形转换回模拟声音,以便通过PC扩音器来播放。
声卡通常还包含MIDI设备。MIDI是符合工业标准的乐器数字化接口(MusicalInstrument DigitalInterface)。这类硬件播放音符以响应短的二进制命令消息。MIDI硬件通常还可以通过电缆连结到如音乐键盘等的MIDI输入设备上。通常,外部的MIDI合成器也能够添加到声卡上。
现在,大多数PC上的CD-ROM驱动器都具备播放普通音乐CD的能力。这就是平常所说的「CD声音」。来自波形声音设备、MIDI设备以及CD声音设备的输出,一般在使用者的控制下用「音量控制」程序混合在一起。
另外几种普遍的多媒体「设备」不需要额外的硬件。Windows视讯设备(也称作AVI视讯设备)播放扩展名为.AVI(audio-videointerleave:声音视频插格)的电影或动画文件。「ActiveMovie控件」可以播放其它型态的电影,包括QuickTime和MPEG。PC上的显示卡需要特定的硬件来协助播放这些电影。
还有个别PC使用者使用某种Pioneer雷射影碟机或者SonyVISCA系列录放机。这些设备都有串行端口接口,因此可由PC软件来控制。某些显示卡具有一种称为「窗口影像(videoin awindow)」的功能,此功能允许一个外部的视讯信号与其它应用程序一起出现在Windows的屏幕上。这也可认为是一种多媒体设备。
API概述
在Windows中,API支持的多媒体功能主要分成两个集合。它们通常称为「低阶」和「高阶」界面。
低阶接口是一系列函数,这些函数以简短的说明性前缀开头,而且在/PlatformSDK/Graphics and Multimedia Services/MultimediaReference/Multimedia Functions(与高阶函数一起)中列出。
低阶的波形声音输入输出函数的前缀是waveIn和waveOut。我们将在本章看到这些函数。另外,本章还讨论用midiOut函数来控制MIDI输出设备。这些API还包括midiIn和midiStream函数。
本章还使用前缀为time的函数,这些函数允许设定一个高分辨率的定时器例程,其定时器的时间间隔速率最低能够到1毫秒。此程序主要用于播放MIDI音乐。其它几组函数包括声音压缩、视讯压缩以及动画和视讯序列,可惜的是本章不包括这些函数。
您还会注意到多媒体函数列表中七个带有前缀mci的函数,它们允许存取媒体控制接口(MCI:MediaControlInterface)。这是一个高阶的开放接口,用于控制多媒体PC中所有的多媒体硬件。MCI包括所有多媒体硬件都共有的许多命令,因为多媒体的许多方面都以磁带录音机这类设备播放/记录方式为模型。您为输入或输出而「打开」一台设备,进而可以「录音」(对于输入)或者「播放」(对于输出),并且结束后可以「关闭」设备。
MCI本身分为两种形式。一种形式下,可以向MCI发送消息,这类似于Windows消息。这些消息包括位编码标记和C数据结构。另一种形式下,可以向MCI发送文字字符串。这个程序主要用于描述命令语言,此语言具有灵活的字符串处理函数,但支持呼叫WindowsAPI的函数不多。字符串命令版的MCI还有利于交互研究和学习MCI,我们马上就举一个例子。MCI中的设备名称包括CD声音(cdaudio)、波形音响(waveaudio)、MIDI编曲器(sequencer)、影碟机(videodisc)、vcr、overlay(窗口中的模拟视频)、dat(digitalaudiotape:数字式录频磁带)以及数字视频(digitalvideo)。MCI设备分为「简单型」和「混合型」。简单型设备(如CD声音)不使用文件。混合型设备(如波形音响)则使用文件。使用波形音响时,这些文件的扩展名是.WAV。
存取多媒体硬件的另一种方法包括DirectXAPI,它超出了本书的范围。
另外两个高阶多媒体函数也值得一提:MessageBeep和PlaySound,它们在第三章有示范。MessageBeep播放「控制台」的「声音」中指定的声音。PlaySound可播放磁盘上、内存中或者作为资源加载的.WAV文件。本章的后面还会用到PlaySound函数。
用TESTMCI研究MCI
在Windows多媒体的早期,软件开发套件含有一个名为MCITEST的C程序,它允许程序写作者交谈式输入MCI命令并学习这些命令的工作方式。这个程序,至少是C语言版,显然已经消失了。因此,我又重新建立了它,即程序22-1所示的TESTMCI程序。虽然我不认为目前程序代码与旧的程序代码有什么区别,但现在的使用者接口还是依据以前的MCITEST程序,并且没有使用现在的程序代码。
程序22-1 TESTMCITESTMCI.C /*--------------------------------------------------------------------------- TESTMCI.C -- MCI Command String Tester(c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_TIMER1 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_EDIT1000 #define IDC_NOTIFY_MESSAGE 1005 #define IDC_NOTIFY_ID 1006 #define IDC_NOTIFY_SUCCESSFUL 1007 #define IDC_NOTIFY_SUPERSEDED1008 #define IDC_NOTIFY_ABORTED 1009 #define IDC_NOTIFY_FAILURE 1010 #define IDC_SIGNAL_MESSAGE 1011 #define IDC_SIGNAL_ID1012 #define IDC_SIGNAL_PARAM 1013 #define IDC_RETURN_STRING1014 #define IDC_ERROR_STRING 1015 #define IDC_DEVICES 1016 #define IDC_STATIC -1
与本章的大多数程序一样,TESTMCI使用非模态对话框作为它的主窗口。与本章所有的程序一样,TESTMCI要求WINMM.LIB引用链接库在MicrosoftVisual C++「Projects Settings」对话框的「Links」页列出。
此程序用到了两个最重要的多媒体函数:mciSendString和mciGetErrorText。在TESTMCI的主编辑窗口输入一些内容然后按下Enter键(或「OK」按钮)后,程序将输入的字符串作为第一个参数传递给mciSendString命令:
error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ;
如果在编辑窗口选择了不止一行,则程序将按顺序将它们发送给mciSendString函数。第二个参数是字符串地址,此字符串取得从函数传回的信息。程序将此信息显示在窗口的「ReturnString」区域。从mciSendString传回的错误代码传递给mciGetErrorString函数,以获得文字错误说明;此说明显示在TESTMCI窗口的「ErrorString」区域。
MCITEXT和CD声音
通过控制CD-ROM驱动器和播放声音CD,您会对MCI命令字符串留下很好的印象。因为这些命令字符串一般都非常简单,并且更重要的是您可以听到一些音乐,所以这是好的起点。您可以在/PlatformSDK/Graphics and Multimedia Services/MultimediaReference/Multimedia CommandStrings中获得MCI命令字符串的参考,以方便本练习。
请确认CD-ROM驱动器的声音输出已连结到扩音器或耳机,然后放入一张声音CD,如BruceSpringsteen的「Born to Run」。Windows98中,「CD播放程序」将启动并开始播放此唱片。如果是这样的话,终止「CD播放程序」,然后可以叫出TESTMCI并且键入命令:
open cdaudio
然后按Enter键。其中open是MCI命令,cdaudio是MCI认定的CD-ROM驱动器的设备名称(假定您的系统中只有一个CD-ROM驱动器。要获得多个CD-ROM驱动器名称需使用sysinfo命令)。
TESTMCI中的「ReturnString」区域显示mciSendString函数中系统传回给程序的字符串。如果执行了open命令,则此值是1。TESTMCI在「ErrorString」区域中显示mciGetErrorString依据mciSendString传回值所传回的信息。如果mciSendString没有传回错误代码,则「ErrorString」区域显示文字"The specified command was carriedout"。
假定执行了open命令,现在就可以输入:
play cdaudio
CD将开始播放唱片上的第一首乐曲「ThunderRoad」。输入下面的命令可以暂停播放:
pause cdaudio
或者
stop cdaudio
对于CD声音设备来说,这些叙述的功能相同。您可用下面的叙述重新播放:
play cdaudio
迄今为止,我们使用的全部字符串都由命令和设备名称组成。其中有些命令带有选项。例如,键入:
status cdaudio position
根据收听时间的长短,「ReturnString」区域将显示类似下面的一些字符:
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 toRun」CD上第五首乐曲是主题曲。MCI命令中的乐曲从1开始编号。要想知道乐曲「Bornto 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(但不是同时使用)。例如,假设您只想播放「BorntoRun」的前10秒,而且播放后,您还想让程序完成其它工作。您可按下面的方法进行(假定您已经将时间格式设定为tmsf):
play cdaudio from 5:0:0 to 5:0:10 wait
这种情况下,直到函数执行结束,也就是说,直到播放完「Born toRun」的前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」(唱片中的最后一首)、「ThunderRoad」和「Born toRun」,并要按此顺序听,可以编写如下的描述命令:
open cdaudio set cdaudio time format tmsf break cdaudio on 27 playcdaudio from 8 wait playcdaudio from 1 to 2 wait playcdaudio from 5 to 6 wait stopcdaudio 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.5Hz到4186 Hz之间,略小于7个八度音阶。
虽然正弦波代表了振动的大多数自然形式,但纯正弦波很少在现实生活中单独出现,而且,纯正弦波并不动听。大多数声音都很复杂。
任何周期的波形(即,一个循环波形)可以分解成多个正弦波,这些正弦波的频率都是整倍数。这就是所谓的Fourier级数,它以法国数学家和物理学家JeanBaptiste JosephFourier(1768-1830)的名字命名。周期的频率是基础。级数中其它正弦波的频率是基础频率的2倍、3倍、4倍(等等)。这些频率的声音称为泛音。基础频率也称作一级谐波。第一泛音是二级谐波,以此类推。
正弦波谐波的相对强度给每个周期的波形唯一的声音。这就是「音质」,它使得喇叭吹出喇叭声,钢琴弹出钢琴声。
人们一度认为电子合成乐器仅仅需要将声音分解成谐波并且与多个正弦波重组即可。不过,事实证明现实世界中的声音并不是这么简单。代表现实世界中声音的波形都没有严格的周期。乐器之间谐波的相对强度是不同的,并且谐波也随着每个音符的演奏时间改变。特别是乐器演奏音符的开始位置-我们称作起奏(attack)-相当复杂,但这个位置又对我们感受音质至关重要。
由于近年来数字储存能力的提高,我们可以将声音直接以数字形式储存而不用复杂的重组。
脉冲编码调制(Pulse CodeModulation)
计算机处理的是数值,因此要使声音进入计算机,就必须设计一种能将声音与数字信号相互转换的机制。
不压缩数据就完成此功能的最常用方法称作「脉冲编码调制」(PCM:pulsecodemodulation)。PCM可用在光盘、数字式录音磁带以及Windows中。脉冲编码调制其实只是一种概念上很简单的处理步骤的奇怪代名词而已。
利用脉冲编码调制,波形可以按固定的周期频率取样,其频率通常是每秒几万次。对于每个样本都测量其波形的振幅。完成将振幅转换成数字信号工作的硬件是模拟数字转换器(ADC:analog-to-digitalconverter)。类似地,通过数字模拟转换器(DAC:digital-to-analogconverter)可将数字信号转换回波形电子信号。但这样转换得到的波形与输入的并不完全相同。合成的波形具有由高频组成的尖锐边缘。因此,播放硬件通常在数字模拟转换器后还包括一个低通滤波器。此滤波器滤掉高频,并使合成后的波形更平滑。在输入端,低通滤波器位于ADC前面。
脉冲编码调制有两个参数:取样频率,即每秒内测量波形振幅的次数;样本大小,即用于储存振幅级的位数。与您想象的一样:取样频率越高,样本大小越大,原始声音的复制品才更好。不过,存在一个提高取样频率和样本大小的极点,超过这个极点也就超过了人类分辨声音的极限。另外,如果取样频率和样本大小过低,将导致不能精确地复制音乐以及其它声音。
取样频率
取样频率决定声音可被数字化和储存的最大频率。尤其是,取样频率必须是样本声音最高频率的两倍。这就是「Nyquist频率(NyquistFrequency)」,以30年代研究取样程序的工程师HarryNyquist的名字命名。
以过低的取样频率对正弦波取样时,合成的波形比最初的波形频率更低。这就是所说的失真信号。为避免失真信号的发生,在输入端使用低通滤波器以阻止频率大于半个取样频率的所有波形。在输出端,数字模拟转换器产生的粗糙的波形边缘实际上是由频率大于半个取样频率的波形组成的泛音。因此,位于输出端的低通滤波器也阻止频率大于半个取样频率的所有波形。
声音CD中使用的取样频率是每秒44,100个样本,或者称为44.1kHz。这个特有的数值是这样产生的:
人耳可听到最高20kHz的声音,因此要拦截人能听到的整个声音范围,就需要40kHz的取样频率。然而,由于低通滤波器具有频率下滑效应,所以取样频率应该再高出大约百分之十才行。现在,取样频率就达到了44kHz。这时,我们要与视讯同时记录数字声音,于是取样频率就应该是美国、欧洲电视显示格速率的整数倍,这两种视讯格速率分别是30Hz和25Hz。这就使取样频率升高到了44.1kHz。
取样频率为44.1kHz的光盘会产生大量的数据,这对于一些应用程序来说实在是太多了,例如对于录制声音而不是录制音乐时就是这样。把取样频率减半到22.05kHz,可由一个10kHz的泛音来简化复制声音的上半部分。再将其减半到11.025kHz就向我们提供了5 kHz频率范围。44.1 kHz、22.05 kHz和11.025kHz的取样频率,以及8 kHz都是波形声音设备普遍支持的标准。
因为钢琴的最高频率为4186Hz,所以您可能会认为给钢琴录音时,11.025kHz的取样频率就足够了。但4186Hz只是钢琴最高的基础频率而已,滤掉大于5000Hz的所有正弦波将减少可被复制的泛音,而这样将不能精确地捕捉和复制钢琴的声音。
样本大小
脉冲编码调制的第二个参数是按位计算的样本大小。样本大小决定了可供录制和播放的最低音与最高音之间的区别。这就是通常所说的动态范围。
声音强度是波形振幅的平方(即每个正弦波一个周期中最大振幅的合成)。与频率一样,人对声音强度的感受也呈对数变化。
两个声音在强度上的区别是以贝尔(以电话发明人Alexander GrahamBell的名字命名)和分贝(dB)为单位进行测量的。1贝尔在声音强度上呈10倍增加。1dB就是以相同的乘法步骤成为1贝尔的十分之一。由此,1dB可增加声音强度的1.26倍(10的10次方根),或者增加波形振幅的1.12倍(10的20次方根)。1分贝是耳朵可感觉出的声强的最小变化。从开始能听到的声音极限到让人感到疼痛的声音极限之间的声强差大约是100dB。
可用下面的公式来计算两个声音间的动态范围,单位是分贝:
其中A1和A2是两个声音的振幅。因为只可能有一个振幅,所以样本大小是1位,动态范围是0。
如果样本大小是8位,则最大振幅与最小振幅之间的比例就是256。这样,动态范围就是:
或者48分贝。48的动态范围大约相当于非常安静的房屋与电动割草机之间的差别。将样本大小加倍到16位产生的动态范围是:
或者96分贝。这非常接近听觉极限和疼痛极限,而且人们认为这就是复制音乐的理想值。
Windows同时支持8位和16位的样本大小。储存8位的样本时,样本以无正负号字节处理,静音将储存为一个值为0x80的字符串。16位的样本以带正负号整数处理,这时静音将储存为一个值为0的字符串。
要计算未压缩声音所需的储存空间,可用以秒为单位的声音持续时间乘以取样频率。如果用16位样本而不是8位样本,则将其加倍,如果是录制立体声则再加倍。例如,1小时的CD声音(或者是在每个立体声样本占2字节、每秒44,100个样本的速度下进行3600秒)需要635MB,这快要接近一张CD-ROM的储存量了。
在软件中产生正弦波
对于第一个关于波形声音的练习,我们不打算将声音储存到文件中或播放录制的声音。我们将使用低阶的波形声音API(即,前缀是waveOut的函数)来建立一个称作SINEWAVE的声音正弦波生成器。此程序以1Hz的增量来生成从20Hz(人可感觉的最低值)到5,000Hz(与人感觉的最高值相差两个八度音阶)的正弦波。
我们知道,标准C执行时期链接库包括了一个sin函数,该函数传回一个弧度角的正弦值(2π弧度等于360度)。sin函数传回值的范围是从-1到1(早在第五章,我们就在SINEWAVE程序中使用过这个函数)。因此,应该很容易使用sin函数生成输出到波形声音硬件的正弦波数据。基本上是用代表波形(这时是正弦波)的数据来填充缓冲区,并将此缓冲区传递给API。(这比前面所讲的稍微有些复杂,但我将详细介绍)。波形声音硬件播放完缓冲区中的数据后,应将第二个缓冲区中的数据传递给它,并且以此类推。
第一次考虑这个问题(而且对PCM也一无所知)时,您大概会认为将一个周期的正弦波分成若干固定数量的样本-例如360个-才合理。对于20Hz的正弦波,每秒输出7,200个样本。对于200Hz的正弦波,每秒则要输出72,000个样本。这有可能实作,但实际上却不能这么做。对于5,000Hz的正弦波,就需要每秒输出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 SINEWAVESINEWAVE.C /*------------------------------------------------------------------------- SINEWAVE.C -- Multimedia Windows Sine Wave Generator(c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include "resource.h" #defineSAMPLE_RATE 11025 #defineFREQ_MIN 20 #defineFREQ_MAX 5000 #defineFREQ_INIT 440 #defineOUT_BUFFER_SIZE 4096 #definePI3.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 PBYTEpBuffer1, 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 callcase MM_WOM_OPEN: SetDlgItemText (hwnd, IDC_ONOFF, TEXT ("Turn Off")) ; // Send two buffers to waveform output deviceFillBuffer (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 SCROLLBARIDC_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_ONOFF1002
注意,FillBuffer例程中用到的OUT_BUFFER_SIZE、SAMPLE_RATE和PI标识符在程序的顶部定义。FillBuffer的iFreq参数是需要的频率,单位是Hz。还要注意,sin函数的结果调整到了0到254的范围之间。对于每个样本,sin函数的fAngle参数都增加一个值,该值的大小是2π弧度乘以需要的频率再除以取样频率。
SINEWAVE的窗口包含三个控件:一个用于选择频率的水平滚动条,一个用于显示目前所选频率的静态文字区域,以及一个标记为「TurnOn」的按钮。按下此按钮后,您将从连结声卡的扩音器中听到正弦波的声音,同时按钮上的文字将变成「TurnOff」。用键盘或者鼠标移动滚动条可以改变频率。要关闭声音,可以再次按下按钮。
SINEWAVE程序代码初始化滚动条,以便频率在WM_INITDIALOG消息处理期间最低是20Hz,最高是5000Hz。初始化时,滚动条设定为440Hz。用音乐术语来说就是中音上面的A,它在管弦乐队演奏时用来调音。DlgProc在接收WM_HSCROLL消息处理期间改变静态变量iFreq。注意,PageLeft和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 audiooutput:波形声音输出句柄)型态的变量。从函数传回时,此变量将设定为一个句柄,后面的波形输出呼叫中将使用该句柄。
waveOutOpen的第二个参数是设备ID。它允许函数可以在安装多个声卡的机器上使用。参数的范围在0到系统所安装的波形输出设备数之间。呼叫waveOutGetNumDevs可以获得波形输出设备数,而呼叫waveOutGetDevCaps可以找出每个波形输出设备。如果想消除设备问号,那么您可以用常数WAVE_MAPPER(定义为-1)来选择设备,该设备在「控制台」的「多媒体」中「音效」页面卷标里的「喜欢使用的设备」中指定。另外,如果首选设备不能满足您的需要,而其它设备可以,那么系统将选择其它设备。
第三个参数是指向WAVEFORMATEX结构的指针(后面将详细介绍)。第四个参数是窗口句柄或指向动态链接库中callback函数的指标,用来表示接收波形输出消息的窗口或者callback函数。使用callback函数时,可在第五个参数中指定程序定义的数据。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)和取样精确度(nBitsPerSample),以及选择单声道或立体声(nChannels)。结构中有些信息看起来是多余的,但该结构也可用于非PCM的取样方式。在非PCM取样方式下,此结构的最后一个字段设定为非0值,并带有其它信息。
对于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),否则传回非0的错误代码。如果waveOutOpen的传回值非0,则SINEWAVE清除窗口,并显示一个标识错误的消息框。
现在设备打开了,SINEWAVE继续初始化两个WAVEHDR结构的字段,这两个结构用于在API中传递缓冲。WAVEHDR定义如下:
typedef struct wavehdr_tag { LPSTR lpData; // pointer to data buffer DWORD dwBufferLength; // length of data buffer DWORD dwBytesRecorded; // used for recorded DWORD dwUser; // for program use DWORD dwFlags; // flags DWORD dwLoops; // number of repetitions struct wavehdr_tag FAR *lpNext; // reserved DWORD reserved; // reserved } WAVEHDR, *PWAVEHDR ;
SINEWAVE将lpData字段设定为包含数据的缓冲区地址,dwBufferLength字段设定为此缓冲区的大小,dwLoops字段设定为1,其它字段都设定为0或NULL。如果要重复循环播放声音,可设定dwFlags和dwLoops字段。
SINEWAVE下一步为两个信息表头呼叫waveOutPrepareHeader函数,以防止结构和缓冲区与磁盘发生数据交换。
到此为止,所有的这些准备都是响应单击开启声音的按钮。但在程序的消息队列里已经有一个消息在等待响应。因为我们已经在函数waveOutOpen中指定要用一个窗口消息处理程序来接收波形输出消息,所以waveOutOpen函数向程序的消息队列发送了MM_WOM_OPEN消息,wParam消息参数设定为波形输出句柄。要处理MM_WOM_OPEN消息,SINEWAVE呼叫FillBuffer函数两次,并用正弦波形数据填充pBuffer缓冲区。然后SINEWAVE把两个WAVEHDR结构传送给waveOutWrite,此函数将数据传送到波形输出硬件,才真正开始播放声音。
当波形硬件播放完waveOutWrite函数传送来的数据后,就向窗口发送MM_WOM_DONE消息,其中wParam参数是波形输出句柄,lParam是指向WAVEHDR结构的指针。SINEWAVE在处理此消息时,将计算缓冲区的新数据,并呼叫waveOutWrite来重新提交缓冲区。
编写SINEWAVE程序时也可以只用一个WAVEHDR结构和一个缓冲区。不过,这样在播放完数据后将会有很短暂的停顿,以等待程序处理MM_WOM_DONE消息来提交新的缓冲区。SINEWAVE使用的「双缓冲」技术避免了声音的不连续。
当使用者单击「TurnOff」按钮关闭声音时,DlgProc接收到另一个WM_COMMAND消息。对此消息,DlgProc把bShutOff变量设定为TRUE,并呼叫waveOutReset函数。此函数停止处理声音并发送一条MM_WOM_DONE消息。bShutOff为TRUE时,SINEWAVE透过呼叫waveOutClose来处理MM_WOM_DONE,从而产生一条MM_WOM_CLOSE消息。处理MM_WOM_CLOSE通常包括清除程序。SINEWAVE为两个WAVEHDR结构而呼叫waveOutUnprepareHeader、释放所有的内存块并把按钮上的文字改回「TurnOn」。
如果硬件继续播放缓冲区的声音数据,那么它自己呼叫waveOutClose就没有作用。您必须先呼叫waveOutReset来停止播放并产生MM_WOM_DONE消息。当wParam是SC_CLOSE时,DlgProc也处理WM_SYSCOMMAND消息,这是因为使用者从系统菜单中选择了「Close」。如果波形声音继续播放,DlgProc则呼叫waveOutReset。无论如何,最后总要呼叫EndDialog来结束程序。
数位录音机
Windows提供了一个称为「录音程序」来录制和播放数字声音。程序22-3所示的程序(RECORD1)不如「录音程序」完善,因为它不含有任何文件I/O,也不允许声音编辑。然而,这个程序显示了使用低阶波形声音API来录制和回放声音的基本方法。
程序22-3 RECORD1RECORD1.C /*--------------------------------------------------------------------------- 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 ; inti ; 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 ; staticTCHAR szOpenError[] = TEXT ("Error opening waveform audio!"); staticTCHAR szMemError [] = TEXT ("Error allocating memory!") ; static WAVEFORMATEX waveform ; switch (message) { case WM_INITDIALOG: // Allocate memory for wave headerpWaveHdr1 = malloc (sizeof (WAVEHDR)) ; pWaveHdr2 = malloc (sizeof (WAVEHDR)) ; // Allocate memory for save buffer pSaveBuffer = malloc (1) ; return TRUE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDC_RECORD_BEG: // Allocate buffer memory pBuffer1 = malloc (INP_BUFFER_SIZE) ; pBuffer2 = 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 = 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 = 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 outputwaveform.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)) { essageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; } return TRUE ; } break ;case MM_WIM_OPEN:// Shrink down the save buffer pSaveBuffer = 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 = 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 = 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 (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.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 (摘录) // Microsoft Developer Studio generated include file. // Used by Record.rc #defineIDC_RECORD_BEG 1000 #defineIDC_RECORD_END 1001 #defineIDC_PLAY_BEG1002 #defineIDC_PLAY_PAUSE 1003 #defineIDC_PLAY_END1004 #defineIDC_PLAY_REV 1005 #defineIDC_PLAY_REP1006 #defineIDC_PLAY_SPEED 1007
RECORD.RC和RESOURCE.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的取样频率播放声音,这样不仅播放速度提高了一倍,而且频率也提高了一个音阶。
录制声音既包括为输入而打开波形声音硬件,还包括将缓冲区传递给API,以便接收声音数据。
RECORD1设有几个内存块。其中三个很小,至少在初始化时很小,并且在DlgProc的WM_INITDIALOG消息处理期间进行配置。程序配置两个WAVEHDR结构,分别由指针pWaveHdr1和pWaveHdr2指向。这两个结构用于将缓冲区传递给波形API。pSaveBuffer指标指向储存整个录音的缓冲区,最初配置时只有一个字节。然后,随着录音的进行,该缓冲区不断增大,以适应所有的声音数据(如果录音时间过长,则RECORD1能够在录制程序中及时发现内存溢出,并允许您重放成功储存的声音)。由于这个缓冲区用来储存堆积的声音数据,所以我将其称为「储存缓冲区(savebuffer)」。指标pBuffer1和pBuffer2指向的另外两个内存块,大小是16K,它们在记录接收的声音数据时配置。录音结束后释放这些内存块。
8个按钮中的每一个都向REPORT1窗口的对话程序DlgProc产生WM_COMMAND消息。最初只有「Record」按钮有效。按下此按钮将产生WM_COMMAND消息,其中wParam参数等于IDC_RECORD_BEG。为处理这个消息,RECORD1配置两个16K的缓冲区来接收声音数据,初始化WAVEFORMATEX结构的字段,并将此结构传递给waveInOpen函数,然后设定两个WAVEHDR结构。
waveInOpen函数产生一条MM_WIM_OPEN消息。在此消息处理期间,RECORD1把储存缓冲区的大小缩减到1个字节,以准备接收数据(当然,第一次录音时,储存缓冲区的大小就是1个字节,但以后录制时,就可能大多了)。在MM_WIM_OPEN消息处理期间,RECORD1也将适当的按钮设定为有效和无效。然后,程序用waveInAddBuffer把两个WAVEHDR结构和缓冲区传送给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。waveInReset函数使录音停止,并产生MM_WIM_DATA消息,该消息含有部分填充的缓冲区。除了呼叫waveInClose来关闭波形输入设备外,RECORD1对这个消息正常响应。
waveInClose产生MM_WIM_CLOSE消息。RECORD1响应此消息时,释放16K输入缓冲区,并使相应的按钮有效或无效。尤其是,当储存缓冲区里存有数据(除非第一次配置就失败,否则一般都含有数据)时,播放按钮将有效。
录音以后,储存缓冲区里将含有这些声音数据。当使用者选择「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只播放一个大缓冲区,因此程序不再简单地准备标题,而是呼叫waveOutClose。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。
另一种MCI界面
您可能已经发现,RECORD1很复杂。特别是在处理波形声音函数呼叫和它们产生的消息间的交互时,更复杂。处理可能出现的内存不足的情况也是如此。但这也许正是它称为低阶界面的原因。我在本章的前面提到过,Windows也提供高阶媒体控制接口(MediaControl Interface)。
对波形声音来说,低阶接口与MCI之间的主要区别在于MCI用波形文件记录声音数据,并通过读取文件来播放声音。由于在播放声音之前要读取文件、处理文件然后再写入文件,所以让RECODE1来实作「特殊效果」很困难。这是典型的折衷选择问题:功能齐全或是使用方便?低阶接口很灵活,但MCI(其中的大部分)更方便。
MCI有两种不同但又相关的实作形式。一种形式用消息和数据结构将命令发送给多媒体设备,然后再从那里接收信息。另一种形式使用ASCII文字字符串。建立文字命令的接口最初是为了让多媒体设备接受简单的描述命令语言的控制。但它也提供非常容易的交谈式控制,请参见本章前面,TESTMCI程序的展示。
RECORD2程序,如程序22-4所示,使用MCI形式的消息和数据结构来实作另一个数字声音录音机和播放器。虽然它使用的对话框模板与RECORD1一样,但并没有实作三个特殊效果的按钮。
程序22-4 RECORD2RECORD2.C /*--------------------------------------------------------------------------- RECORD2.C -- Waveform Audio Recorder(c) Charles Petzold, 1998 ------------------------------------------------------------------------*/ #include <windows.h> #include "..\\record1\\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 BOOLbRecording, bPlaying, bPaused ; static TCHAR szFileName[] = TEXT ("record2.wav") ; static WORDwDeviceID ; DWORD dwError ; MCI_GENERIC_PARMS mciGeneric ; MCI_OPEN_PARMSmciOpen ; MCI_PLAY_PARMSmciPlay ; MCI_RECORD_PARMS mciRecord ; MCI_SAVE_PARMSmciSave ; 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 buttonsEnableWindow (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 buttonsEnableWindow (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")) ; Paused = 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, dwParam)
第一个参数是设备的识别数字(ID),您可以按句柄来使用ID。打开设备时就可以获得ID,并在随后的mciSendCommand呼叫中使用。第二个参数是前缀为MCI的常数。这些称为MCI命令消息,RECORD2展示了其中的七个:MCI_OPEN、MCI_RECORD、MCI_STOP、MCI_SAVE、MCI_PLAY、MCI_PAUSE和MCI_CLOSE。
dwFlags参数通常由0或者多个位旗标常数(由C的位OR运算子合成)组成。这些通常用来表示不同的选项。一些选项是某个命令消息所特有的,而另一些对所有的消息都是通用的。dwParam参数通常是指向一个数据结构的长指针,该结构表示选项以及由设备获得的信息。许多MCI消息都与数据结构有关,而且这些数据结构对于消息来说都是唯一的。
如果mciSendCommand函数呼叫成功,则传回0值,否则传回错误代码。要向使用者报告此错误,可用下面的函数获得描述错误的文字字符串:
mciGetErrorString (error, szBuffer, dwLength)
此函数在程序TESTMCI中也用到过。
按下「Record」按钮后,RECORD2的窗口消息处理程序就收到一个WM_COMMAND消息,其中wParam等于IDC_RECORD_BEG。RECORD2从打开设备开始,包括设定MCI_OPEN_PARMS结构的字段,并用MCI_OPEN命令消息呼叫mciSendCommand。录音时,lpstrDeviceType字段设定为字符串「waveaudio」以说明设备型态,lpstrElementName字段设定为长度为0的字符串。MCI驱动程序使用内定的取样频率和取样精确度,但是您可以用MCI_SET命令进行修改。录音程序中,声音数据先储存在硬盘上的临时文件中,最后再转化成标准的波形文件。本章的后面将介绍波形文件的格式。播放录制的声音时,MCI使用波形文件中定义的取样频率和取样精确度。
如果RECORD2不能打开设备,则用mciGetErrorString和MessageBox提示错误信息。否则从mciSendCommand呼叫传回,MCI_OPEN_PARMS结构的wDeviceID字段包含有设备ID,以供后面的呼叫使用。
要开始录音,RECORD2就呼叫mciSendCommand,以MCI_RECORD命令消息和MCI_WAVE_RECORD_PARMS数据结构为参数。当然,您也可以将此结构(并使用表示这些字段已设定的位旗标)的dwFromz和dwTo字段进行设定,以便将声音插入现有的波形文件,其文件名在MCI_OPEN_PARMS结构的lpstrElementName字段指定。内定状态下,任何新的声音都插入在现有文件的开始位置。
RECORD2将MCI_WAVE_RECORD_PARMS结构的dwCallback字段设定为程序的窗口句柄,并在mciSendCommand呼叫中包含MCI_NOTIFY标记。这导致录音结束后向窗口消息处理程序发送一条通知消息。我将简要讨论一下这条通知消息。
录音结束后,按下前一个「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字段是一个有效的文件名。通过文件的扩展名称.WAV,MCI知道使用者要打开一个波形声音设备。如果存在多个波形硬件,则打开第一个(设定MCI_OPEN_PARMS结构的lpstrDeviceType字段,也可以打开其它波形设备)。
播放将包括带有MCI_PLAY命令消息和MCI_PLAY_PARMS结构的mciSendCommand呼叫。虽然波形文件的任意部分都可以播放,但RECORD2只播放整个文件。
RECORD2还包括一个「Pause」按钮来暂停播放声音文件。这个按钮产生一个WM_COMMAND消息,其中wParam等于IDC_PLAY_PAUSE。响应时,程序将呼叫mciSendCommand,并以MCI_PAUSE命令消息和MCI_GENERIC_PARMS结构作为参数。MCI_GENERIC_PARMS结构用于这样一些消息:它们除了需要用于通知的可选窗口句柄外,不需要任何信息。如果播放已经暂停,则通过再次使用MCI_PLAY命令消息呼叫mciSendCommand继续播放。
按下第二个「End」按钮也可以停止播放。这时产生wParam等于IDC_PLAY_END的WM_COMMAND消息。响应时,窗口消息处理程序将呼叫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(虽然现在还不能说它很完美,但其功能已经很齐全了)。响应时,窗口消息处理程序给自己发送一个WM_COMMAND消息,其中wParam等于IDC_RECORD_END,然后与正常情况下一样:停止录音、储存文件并关闭设备。
MCI命令字符串的方法
Windows的多媒体接口曾经包含函数mciExecute,其语法如下:
bSuccess = mciExecute (szCommand) ;
其中唯一的参数是MCI命令字符串。函数传回布尔值-如果呼叫成功,则传回非0值,否则传回0。在功能上,mciExecute函数相同于呼叫后三个参数为NULL或0的mciSendString(TESTMCI中使用的依据字符串的MCI函数),然后在发生错误时呼叫mciGetErrorString和MessageBox。
虽然mciExecute不再是API的一部分,但我还是在RECORD3版的数字录音机中使用了这个函数。和RECORD2一样,RECORD3程序也使用RECORD1中的资源描述档RECORD.RC和RESOURCE.H,如程序22-5所示。
程序22-5 RECORD3RECORD3.C /*--------------------------------------------------------------------------- RECORD3.C -- Waveform Audio Recorder (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "..\\record1\\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 ; TCHARszErrorStr [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将命令字符串转换为相应的命令消息和数据结构。RECORD3可以使用像RECORD2一样使用MM_MCINOTIFY消息,但是它没有选择mciExecute函数的好处,它的缺点是程序不知道什么时候播放完波形文件。因此,这些按钮不能自动改变状态。您必须人工按下「End」按钮,以便让程序知道它已经准备再次录音或播放。
注意MCI的open命令中alias关键词的用法。它允许所有后来的MCI命令使用别名来引用设备。
波形声音文件格式
如果在十六进制转储程序下研究未压缩的.WAV文件(即PCM),您会发现它们具有表22-1所示的格式。
表22-1 .WAV文件格式 |
偏移量 | 字节 | 资料 |
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 FileFormat:资源交换文件格式)的格式。RIFF是用于多媒体数据文件的万用格式,它是一种标记文件格式。在这种格式下,文件由数据「块」组成,而这些数据块则由前面4个字符的ASCII名称和4字节(32位)的数据块大小来确认。数据块大小值不包括名称和大小所需要的8字节。
波形声音文件以文字字符串「RIFF」开始,用来标识这是一个RIFF文件。字符串后面是一个32位的数据块大小,表示文件其余部分的大小,或者是小于8字节的文件大小。
数据块以文字字符串「WAVE」开始,用来标识这是一个波形声音块,后面是文字字符串「fmt」-注意用空白使之成为4字符的字符串-用来标识包含波形声音数据格式的子数据块。「fmt」字符串的后面是格式信息大小,这里是16字节。格式信息是WAVEFORMATEX结构的前16个字节,或者,像最初定义时一样,是包含WAVEFORMAT结构的PCMWAVEFORMAT结构。
nChannels字段的值是1或2,分别对应于单声道和立体声。nSamplesPerSec字段是每秒的样本数;标准值是每秒11,025、22,050和44100个样本。nAvgBytesPerSec字段是取样速率,单位是每秒样本数乘以信道数,再乘以以位为单位的每个样本的大小,然后除以8并往上取整数。标准样本大小是8位和16位。nBlockAlign字段是信道数乘以以位为单位的样本大小,然后除以8并往上取整数。最后,该格式以wBitsPerSample字段结束,该字段是信道数乘以以位为单位的样本大小。
格式信息的后面是文字字符串「data」,然后是32位的数据大小,最后是波形数据本身。这些数据是按相同格式进行简单连结的样本,这与低阶波形声音设备上所使用的格式相同。如果样本大小是8位,或者更少,那么每个样本有1字节用于单声道,或者有2字节用于立体声。如果样本大小在9到16位之间,则每个样本就有2字节用于单声道,或者4字节用于立体声。对于立体声波形数据,每个样本都由左值及其后面的右值组成。
对于8位或不到8位的样本大小,样本字节被解释为无正负号值。例如,对于8位的样本大小,静音等于0x80字节的字符串。对于9位或更多的样本大小,样本被解释为有正负号值,这时静音的字符串等于值0。
用于读取标记文件的一个重要规则是忽略不准备处理的数据块。尽管波形声音文件需要「fmt」和「data」子数据块(按照此顺序),但它还包含其它子数据块。尤其是,波形声音文件可能包含一个标记为「INFO」的子数据块,和提供波形声音文件信息的子数据块的子数据块。
迭加合成实验
许多年来-至少从毕达哥拉斯的年代起-人们就已经试图分析音调。起初好像非常简单,但随后就变得复杂了。抱歉,我将重复一些已经说过的有关声音的问题。
音调,除了一些撞击声以外,都有特殊的音调或频率。这个频率可以在人类能够感受到的频谱范围内,也就是从20Hz到20,000Hz以内。例如,钢琴的频率范围在27.5Hz到4186Hz之间。音调的另一个特征是音量或响度。这与产生音调的波形的所有振幅相对应。响度的变化用分贝度量。迄今为止,一切都很好。
然后有一件难办的事称做「音质」。非常简单,音质就是声音的性质,利用它,我们可以区分按相同音调相同音量演奏的钢琴、小提琴和喇叭。
法国数学家Fourier发现一些周期性的波形-不论多么复杂-它们都可以表示为许多频率是基础频率整数倍的正弦波形。这个基础频率,也称作第一个谐波,是波形周期的频率。第一个泛音,也称作二级谐波,是基本频率的两倍;第二个泛音,或者三级谐波的频率是基本频率的三倍,依次类推。谐波振幅的相互关系形成了波形的形状。
例如,方波可以表示为许多的正弦波,其中偶数谐波(即2、4、6等等)的振幅都是0,而奇数谐波(即1、3、5等等)的振幅都按1、1/3、1/5比例依次类推。在锯齿波中,所有的泛音都出现,而振幅都按1、1/2、1/3、1/4比例依此类推。
对于德国科学家HermannHelmholtz(1821-1894),这是了解音质的关键。在他的名着《On theSensations of Tone》(1885年,1954年由DoverPress再版)中,Helmholtz假定耳朵和大脑将复杂的声音分解为正弦波,而这些正弦波相关的强度就是我们所感受的音质。不幸的是,事情还没有这么简单。
随着1968年Wendy Carlos的唱片《Switched onBach》的发布,电子音乐合成器引起了公众的广泛注意。那时使用的合成器(例如Moog)是模拟合成器。这些合成器使用模拟电路来产生各种声音波形,例如方波、三角波形和锯齿波形。要使这些波形听起来更像真实的乐器,它们取决于单个音符的变化程序。波形的所有振幅以「包络(envelope)」形成。当音符开始时,振幅由0开始增加,通常增加非常快。这就是所谓的起奏。然后当音符持续时,振幅保持为常数,这时称为持续。音符结束时,振幅降为0,这时称为释放。
波形通过滤波器,滤波器将削弱一些谐波,并将简单波形转换得更复杂、更有乐感。这些滤波器的切断频率由包络控制,以便声音的谐波内容在音符的程序中改变。
因为这些合成器以丰富的波形格式调和开始,而且一些谐波通过滤波器进行了削弱,这种形式的合成称为「负合成」。
即使在负合成期间,许多人也还会在电子音乐中发现迭加合成是下一个大问题。
在迭加合成中,您可以从许多整数倍正弦波生成器开始,选择整数倍以便于每个正弦波都对应一个谐波。每个谐波的振幅都由一个包络单独控制。使用模拟电路的迭加合成不实用,因为对单个音符就需要8和24之间数目的正弦波生成器,而与这些正弦波生成器相关的频率必须精确的互相对齐。模拟波形生成器稳定性很差,而且容易发生频率漂移。
不过,由数字合成器(可以数字化地使用对照表产生波形)和计算机产生的波形,频率漂移并不是个问题,因而迭加合成也就切实可行了。因此总的来说:在录制真实的乐曲时,可以用Fourier分解法将其分解成多个谐波。然后就可以确定每个谐波的相对强度,再用多个正弦波数字化地产生声音。
如果开始实验时用Fourier分析法分析实际的音调,并从多个正弦波来产生这些音调,那么人们将发现音质并不像Helmholtz所认为的那样简单。
最大的问题是真实音调的谐波之间并没有精确的整数关系。事实上,「谐波」一词对于实际的音调来说并不十分适当。各种正弦波组成都不和谐,或者更准确地说是「泛音」。
人们发现,实际音调泛音之间的不和谐在创造「真实的」声音时很重要。静态和谐会产生「电流」声。每个泛音都在单个音符上改变振幅和频率。泛音中,相对频率和振幅的关系对于不同的泛音以及来自相同乐器的不同强度是不同的。实际音调中最复杂的部分发生在音符的起奏部分,这时比较不和谐。人们发现音符的这个复杂的起奏位置对于人类感受音质很重要。
简而言之,实际乐器的声音比任何想象的都更复杂。分析音调的观点,以及后面用于控制泛音的振幅和频率的相对简单的包络观点显然都不实用。
实际乐曲的一些分析法发表于早期(1977到1978年间)的《ComputerMusic Journal》(当时由People's ComputerCompany发行,现在由MIT Press发行)由James A. Moorer、JohnGrey和John Strawn Some编写了第三部分丛书《Lexicon of AnalyzedTones》,该书显示了在小提琴、双簧管、单簧管和喇叭上演奏一个音符(小于半秒种)的泛音的振幅和频率图形。所用的音符是中音C上的降E。小提琴用20个泛音,双簧管和单簧管用21个,而喇叭用12个。实际上,《ComputerMusic Journal》的Volume II、Number2(1978年9月)包含了用线段来近似双簧管、单簧管和喇叭的不同频率和振幅的包络。
因此,利用Windows上支持的声音波形功能,下面的程序很简单:将这些数字键入程序、为每个泛音都产生多个正弦波样本、添加这些样本并将其发送给波形声音声卡,因此把20年前原始录制的声音重新制造出来也很容易。ADDSYNTH(「迭加合成」)如程序22-6所示。
程序22-6 ADDSYNTHADDSYNTH.C /*-------------------------------------------------------------------------- ADDSYNTH.C -- Additive Synthesis Sound Generation(c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include "addsynth.h" #include "resource.h" #defineID_TIMER 1 #defineSAMPLE_RATE 22050 #defineMAX_PARTIALS 21 #definePI3.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) { DWORDdwWritten ; 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 = 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 (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.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 (摘录) // Microsoft Developer Studio generated include file. // Used by AddSynth.rc #defineIDC_TRUMPET 1000 #defineIDC_OBOE 1001 #defineIDC_CLARINET 1002 #defineIDC_TEXT1003
这里没有给出附加文件ADDSYNTH.H,因为它包含几百行令人讨厌的叙述,您将在本书附上的光盘上找到它。在ADDSYNTH.H的开始位置,我定义了三个结构,用于储存包络数据。每个振幅和频率分别储存到型态ENV的结构数组中。这些数字对由时间(毫秒)和振幅值(按任意度量单位)或频率(以周期/秒为单位)组成。这些数组的长度可变,其变化范围从6到14。假定振幅和频率值之间直接相关。
每种乐器都包括一个泛音集(喇叭用12个,双簧管和单簧管分别使用21个),这些泛音集储存在型态PRT的结构数组中。PRT结构储存振幅和频率包络的点数,以及指向ENV数组的指针。INS结构包括音调的总时间(以毫秒为单位)、泛音数以及指向储存泛音的PRT数组的指针。
ADDSYNTH有三个标记为「Trumpet」、「Oboe」和「Clarinet」的按钮。PC的速度还没有快到足以实时计算所有的迭加合成,因此第一次执行ADDSYNTH时,这些按钮将失效,直到程序计算完样本并建立了TRUMPET.WAV、OBOE.WAV和CLARINET.WAV声音文件后,按钮才启动,而且可以使用PlaySound函数播放这三种声音。下次执行时,程序将检查波形文件是否存在,而不需重新建立。
ADDSYNTH中的FillBuffer函数完成了大多数工作。FillBuffer从计算合成最大振幅的总数开始。为此,它在乐器的泛音中循环,以找出每个泛音的最大振幅,然后将所有的最大振幅加起来。此值后来用于将样本缩放到8位的样本大小。
然后FillBuffer计算每个样本的值。每个样本都对应于一段以毫秒为单位的时间,该时间取决于取样频率(实际上,在22.05kHz的取样频率下,每22个样本对应于相同的毫秒时间值)。然后,FillBuffer在泛音中循环。对于频率和振幅,它找出与毫秒时间值对应的包络线段,并执行线性插补。
频率值与相位角值一起传递给SineGenerator函数。本章前面讨论过,产生数字化的正弦波形需要保持相位角值,并依据频率值增加。从SineGenerator函数传回时,正弦值将乘以泛音的振幅并累加。样本的所有泛音都加在起来之后,样本就缩放到字节大小。
起床号波形声音
WAKEUP,如程序22-7所示,是原始码文件看起来不是很完整的程序之一。程序窗口看起来像对话框,但是没有资源描述档(我们已经知道如何编写),并且程序使用一个波形档案,但在光盘上却没有这样的档案。不过,程序非常有趣:它播放的声音很大,并且非常令人讨厌。WAKEUP是我的闹钟,能够唤醒我继续工作。
程序22-7 WAKEUPWAKEUP.C /*--------------------------------------------------------------------------- WAKEUP.C -- Alarm Clock Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <commctrl.h> // ID values for 3 child windows #defineID_TIMEPICK 0 #defineID_CHECKBOX 1 #defineID_PUSHBTN2 // Timer ID #defineID_TIMER 1 // Number of 100-nanosecond increments (ie FILETIME ticks) in an hour #define FTTICKSPERHOUR (60 * 60 * (LONGLONG) 10000000) // Defines and structure for waveform "file" #defineSAMPRATE 11025 #defineNUMSAMPS (3 * SAMPRATE) #defineHALFSAMPS (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 ; HINSTANCEhInstance ; INITCOMMONCONTROLSEX icex ; int i, cxChar, cyChar ; LARGE_INTEGER li ; SYSTEMTIMEst ; 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] = 230 ; else if (i % 8 < 4) pwaveform->byData[i] = 25 ; else pwaveform->byData[i] = 230 ; // Get character size and set a fixed window 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 disables 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 outstanding 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使用称为「Date-TimePicker」的通用控件。这个控件用来让使用者选择指定的日期和时间(WAKEUP只使用时间挑选功能)。程序可以使用SYSTEMTIME结构来获得和设定时间,在获得和设定PC自身时钟时也使用该结构。要多方面了解Date-TimePicker,请试着建立不带有任何DTS样式旗标的窗口。
注意WM_CREATE消息结束时的处理方式:程序假定您在睡觉之前执行它,并希望它在8小时之后来唤醒您。
现在很明显,可以从GetLocalTime函数在SYSTEMTIME结构获得目前时间,而且可以「手工」增加时间。但在一般情况下,此计算将涉及检查大于24小时的结果时间,这意味着您必须增加天数字段,然后可能涉及增加月(因此还必须有用于每月天数和闰年检查的逻辑),最后您可能还要增加年。
事实上,推荐的方法(来自/Platform SDK/Windows BaseServices/General Library/Time/Time Reference/TimeStructures/SYSTEMTIME)是将SYSTEMTIME转换为FILETIME结构(使用SystemTimeToFileTime),将FILETIME结构强制转换为LARGE_INTEGER结构,在大整数上执行计算,再强制转换回FILETIME结构,然后转换回SYSTEMTIME结构(使用FileTimeToSystemTime)。
顾名思义,FILETIME结构用于获得和设定文件最后一次更新的时间。此结构如下:
type struct _FILETIME // ft { DWORD dwLowDateTime ; DWORD dwHighDateTime ; } FILETIME ;
这两个字段一起表示了从1601年1月1日起每隔1000亿分之一秒所显示的64位值。
Microsoft C/C++编译器支持64位整数作为ANSIC的非标准延伸语法。数据型态是__int64。您可以对__int64型态执行所有的常规算术运算,并且有一些执行时期链接库函数也支持它们。Windows的WINNT.H表头文件定义如下:
typedef __int64 LONGLONG ; typedef unsigned __int64 DWORDLONG ;
在Windows中,这有时称为「四字组」,或者更普遍地称为「大整数」。也有一个union定义如下:
typedef union _LARGE_INTEGER { struct { DWORD LowPart ; LONG HighPart ; } ; LONGLONG QuadPart ; } LARGE_INTEGER ;
这是/Platform SDK/Windows Base Services/General Library/LargeIntegerOperations中的全部文件。此union允许您使用32位或者64位的大整数。
MIDI 和音乐
由电子音乐合成器制造者协会在19世纪80年代早期开发了「乐器数字化接口」(MIDI:MusicalInstrument DigitalInterface)。MIDI是用于将它们中的电子乐器与计算机连结起来的协议,也是电子音乐领域中相当重要的标准。MIDI规范由MIDIManufacturers Association(MMA)维护,它的网站是http://www.midi.org。
使用MIDI
MIDI为透过电缆来传递数字命令定义了传输协议。MIDI电缆使用5针DIN接头,但是只使用了三个接头。一个是屏蔽,一个是回路,而第三个传输数据。MIDI协议在每秒31,250位的速度下是单向的。数据的每个字节都由一个开始位开始,以一个停止位结束,用于每秒3,125字节的有效传输速率。
重要的是要了解真实的声音-不论是模拟格式还是数字格式-不是经由MIDI电缆传输的。通过电缆传输的通常都是简单的命令消息,长度一般是1、2或3字节。
简单的MIDI设定可以包括两片MIDI兼容硬件。一个是本身不发声,但是单独产生MIDI消息的MIDI键盘。此键盘有一个有标记有「MIDIOut」的MIDI端口。用MIDI电缆将这个埠与MIDI声音合成器的「MIDIIn」埠连结起来。合成器看起来很像前面有几个按钮的小盒子。
按下键盘上的一个键时(假定是中音C),键盘就将3个字节发送给MIDIOut端口。在十六进制中,这些字节是:
90 3C 40
第一个字节(90)显示NoteOn消息。第二个字节是键号,其中3C是中音C。第三个字节是敲按键的速度,此速度范围是从1到127。我们恰巧使用了一个对速度不敏感的键盘,因此它发送平均速度值。这个3字节的消息顺着MIDI电缆进入合成器的MidiIn埠。通过播放中音C的音调来响应合成器。
释放键时,键盘会将另一个3字节消息发送给MIDI Out端口:
90 3C 00
这与Note On命令相同,但带有0速字节。这个字节值0表示NoteOff命令,意味着应该关闭音符。合成器通过停止声音来响应。
如果合成器有复调音乐的能力(即,同时播放多个音符的能力),那么您就可以在键盘上演奏和弦。键盘产生多条NoteOn消息,并且合成器将播放所有的音符。当您释放和弦时,键盘就将多条NoteOff消息发送给合成器。
一般来说,这种设定中的键盘称为「MIDI控制器」,它负责产生MIDI消息来控制合成器。MIDI控制器看起来不像键盘。MIDI控制器包括下面几种:看起来像单簧管或萨克斯管的MID管乐控制器、MIDI吉他控制器、MIDI弦乐控制器和MIDI鼓控制器。至少所有这些控制器都产生3字节的NoteOn和Note Off消息。
胜过类似的键盘或传统乐器,控制器也可以是「编曲器」,它是在内存中储存NoteOn和NoteOff消息顺序,然后再播放的硬件。现在单机编曲器已经比几年前少见多了,因为它们已经被计算机所替代。安装MIDI卡的计算机也可以生成NoteOn和NoteOff消息来控制合成器。MIDI编辑软件,允许您在屏幕上作曲,还可以储存来自MIDI控制器的MIDI消息,并处理这些消息,然后将MIDI消息发送给合成器。
合成器有时也称为「声音模块(sound module)」或「音源器(tonegenerator)」。MIDI不指定如何真正产生这些声音的方法。合成器可以使用任何一种声音生成技术。
实际上,只有非常简单的MIDI控制器(例如管乐控制器)才只有MIDIOut电缆埠。通常键盘都有内建合成器,并且有三个MIDI电缆端口,分别标记为「MIDIIn」、「MIDI Out」和「MIDI Thru」。MIDIIn端口接受MIDI消息,从而播放键盘的内部合成器。MIDIOut端口将MIDI消息从键盘发送到外部合成器。MIDIThru埠是一个输出埠,它复制MIDI In端口的输入信号-无论从MIDIIn埠获得什么都发送给MIDI Thru埠(MIDI Thru埠不包括从MIDIOut埠发送的任何信息)。
透过电缆连结MIDI硬件只有两种方法:将一个硬件上的MIDIOut连结到另一个的MIDI In,或者将MIDI Thru与MIDI In连结。MIDIThru端口允许连结一系列的MIDI合成器。
程序更改
合成器能制作哪种声音?是钢琴声、小提琴声、喇叭声还是飞碟声?通常合成器能够生成的各种声音都储存在ROM或者其它地方。它们通常称为「声音」、「乐器」或者「音色」。(「音色」一词来自模拟合成器的时代,当时通过将音色和弦插入合成器前面的插孔中来设定不同的声音)。
在MIDI中,合成器能够生成的各种声音称为「程序」。改变这个程序需要向合成器发送MIDIProgram Change消息
C0 pp
其中,pp的范围是0到127。通常MIDI键盘的顶部是一系列有限的按钮,这些按钮将产生ProgramChange消息。透过按下这些按钮,您可以从键盘控制合成器的声音。这些按钮号通常由1开始,而不是由0开始,因此程序句柄1与ProgramChange字节的0对应。
MIDI规格没有说明程序句柄与乐器的对应关系。例如,着名的YamahaDX7合成器上的前三个程序分别称为「Warm Strings」、「 MellowHorn」和「Pick Guitar」。而在YamahaTX81Z音调发生器上,它们是Grand Piano、Upright Piano和DeepGrand。在Roland MT-32声音模块上,它们是Acoustic Piano 1、AcousticPiano 2和Acoustic Piano3。因此,如果不希望在从键盘制作程序改变时感到吃惊,那么最好了解一下乐器声与您将使用的合成器的程序句柄的对应关系。
这对于包含ProgramChange消息的MIDI文件来说是一个实际问题-这些文件并不是设备无关的,因为它们的内容在不同的合成器上听起来是不一样的。然而,在最近几年,「GeneralMIDI」(GM)标准已经把这些程序句柄标准化。Windows支援GeneralMIDI。如果合成器与GeneralMIDI规格不一致,那么程序转换可使它仿真General MIDI合成器。
MIDI通道
迄今为止,我已经讨论了两条MIDI消息,第一条是Note On:
90 kk vv
其中,kk是键号(0到127),v v是速度(0到127)。0速度表示NoteOff命令。第二条是Program Change:
C0 pp
其中,pp的范围是从0到127。这些是典型的MIDI消息。第一个字节称作「状态」字节。根据字节的状态,它通常后跟0、1或2字节的「数据」(我即将说明的「系统专有」消息除外)。从数据字节中分辨出状态字节很容易:高位总是1用于状态字节,0用于数据字节。
然而,我还没有讨论过这两个消息的普通格式。NoteOn消息的普通格式如下:
9n kk vv
而Program Change是:
Cn pp
在这两种情况下,n表示状态字节的低四位,其变化范围是0到15。这就是MIDI「通道」。通道一般从1开始编号,因此,如果n为0,则代表通道1。
使用16个不同通道允许一条MIDI电缆传输16种不同声音的消息。通常,您将发现MIDI消息的特殊字符串以ProgramChange消息开始,为所用的不同信道设定声音,而字符串的后面是多条NoteOn和Note Off命令。再后面可能是其它的ProgramChange命令。但任何时候,每个通道都只与一种声音联系。
让我们作一个简单范例:假定我已经讨论过的键盘控制能够同时产生用于两条不同信道-信道1和信道2-的MIDI消息。透过按下键盘上的按钮将两条ProgramChange消息发送给合成器:
C0 01 C1 05
现在设定信道1用于程序2,并设定信道2用于程序6(回忆信道句柄和程序句柄都是基于1的,但消息中的编码是基于0的)。现在按下键盘上的键时,就发送两条NoteOn消息,一条用于一个通道:
90 kk vv 91 kk vv
这就允许您和谐地同时播放两种乐器的声音。
另一种方法是「分开」键盘。低键可以在信道1上产生NoteOn消息,高键可以在信道2上产生NoteOn消息。这就允许您在一个键盘上独立播放两种乐器的声音。
当您考虑PC上的MIDI编曲软件时,使用16个通道将更为有利。每个通道都代表不同的乐器。如果有能够独立播放16种不同乐器的合成器,那么您就可以编写用于16个波段的管弦乐曲,而且只使用一条MIDI电缆将MIDI卡与合成器连结起来。
MIDI消息
尽管Note On和ProgramChange消息在任何MIDI执行中都是最重要的消息,但并不是所有的MIDI都可以执行。表22-2是MIDI规格中定义的MIDI信道消息表。我在前面提到过,状态字节的高位总是设定着,而状态字节后面的数据字节的高位都等于0。这意味着状态字节的范围是0x80到0xFF,而数据字节的范围是0到0x7F。
表22-2 MIDI信道消息(n =信道句柄,从0到15) |
MIDI消息 | 数据字节 | 值 |
Note Off | 8n kk vv | kk = 键号(0-127) vv = 速度(0-127) |
Note On | 9n kk vv | kk = 键号(0-127) vv = 速度(1-127, 0 = note off) |
Polyphonic After Touch | An kk tt | kk = 键号(0-127) tt = 按下之后 (0-127) |
Control Change | Bn cc xx | cc = 控制器(0-121) xx = 值(0-127) |
Channel Mode Local Control | Bn 7A xx | xx = 0(关),127(开) |
All Notes Off | Bn 7B 00 | |
Omni Mode Off | Bn 7C 00 | |
Omni Mode On | Bn 7D 00 | |
Mono Mode On | Bn 7E cc | cc = 频道数 |
Poly Mode On | Bn 7F 00 | |
Program Change | Cn pp | pp = 程序(0-127) |
Channel After Touch | Dn tt | tt = 按下之后(0-127) |
Pitch Wheel Change | En ll hh | ll = 低7位(0-127) hh = 高7位(0-127) |
虽然没有严格的要求,键号通常还是与西方音乐的传统音符相对应(例如,对于打击声音,每个键号码可以是不同的打击乐器)。当键号与钢琴类的键盘对应时,键60(十进制)是中音C。MIDI键号在普通的88键钢琴范围的基础上向下扩展了21个音符,向上扩展了19个音符。速度句柄是按下某键的速度,在钢琴上它控制声音的响度与和谐特征。特殊的声音可以依这种方式或其它方式来响应键的速度。
前面展示的例子使用带有0速度字节的Note On消息来表示NoteOff命令。对于键盘(或者其它控制器)还有一个单独的NoteOff命令,该命令实作释放键的速度,不过,非常少见。
还有两个「接触后」消息。接触后是一些键盘的特征,按下某个键以后,再用力按下键可以在某些方式上改变声音。一个消息(状态字节0xDn)是将接触后应用于通道中目前演奏的所有音符,这是最常见的。状态字节0xAn表示独立应用每个单独键的接触后。
通常,键盘上都有一些用于进一步控制声音的刻度盘或开关。这些设备称为「控制器」,所有变化都由状态字节0xBn表示。通过从0到121的号码确认控制器。0xBn状态字节也用于ChannelMode消息,这些消息显示了合成器如何在通道中响应同时发生的音符。
一个非常重要的控制器是上下转换音调的轮,它有一个单独的MIDI消息,其状态字节是0xEn。
表22-2中所缺少的是状态字节以从F0到FF开始的消息。这些消息称为系统消息,因为它们适用于整个MIDI系统,而不是部分通道。系统消息通常用于同步的目的、触发编曲器、重新设定硬件以及获得信息。
许多MIDI控制器连续发送状态字节0xFE,该字节称为ActiveSensing消息。这简单地表示了MIDI控制器仍依附于系统。
一条重要的系统消息是以状态字节0xF0开始的「系统专用」消息。此消息用于将数据块按厂商与合成器所依靠的格式传递给合成器(例如,用这种方法可以将新的声音定义从计算机传递给合成器)。系统专用消息只是可以包含多于2个数据字节的唯一消息。实际上,数据字节数是变化的,而每个数据字节的高位都设定为0。状态字节0xF7表示系统专用消息的结尾。
系统专用消息也用于从合成器转储数据(例如,声音定义)。这些数据都是通过MIDIOut端口来自合成器。如果要用设备无关的方式对MIDI编写程序,则应该尽可能避免使用系统专用消息。但是它们对于定义新的合成器声音是非常有用的。
MIDI文件(扩展名是.MDI)是带有定时信息的MIDI信息集,可以用MCI播放MIDI文件。不过,我将在本章的后面讨论低阶midiOut函数。
MIDI编曲简介
低阶MIDI的API包括前缀为midiIn和midiOut的函数,它们分别用于读取来自外部控制器的MIDI序列和在内部或外部的合成器上播放音乐。尽管其名称为「低阶」,但使用这些函数时并不需要了解MIDI卡上的硬件接口。
要在播放音乐的准备期间打开一个MIDI输出设备,可以呼叫midiOutOpen函数:
error = midiOutOpen (&hMidiOut, wDeviceID, dwCallBack,dwCallBackData, dwFlags) ;
如果呼叫成功,则函数传回0,否则传回错误代码。如果参数设定正确,则常见的一种错误就是MIDI设备已被其它程序使用。
该函数的第一个参数是指向HMIDIOUT型态变量的指针,它接收后面用于MIDI输出函数的MIDI输出句柄。第二个参数是设备ID。要使用真实的MIDI设备,这个参数范围可以是从0到小于由midiOutGetNumDevs传回的数值。您还可以使用MIDIMAPPER,它在MMSYSTEM.H中定义为-1。大多数情况下,函数的后三个参数设定为NULL或0。
一旦打开一个MIDI输出设备并获得了其句柄,您就可以向该设备发送MIDI消息。此时可以呼叫:
error = midiOutShortMsg (hMidiOut, dwMessage) ;
第一个参数是从midiOutOpen函数获得的句柄。第二个参数是包装在32位DWORD中的1字节、2字节或者3字节的消息。我在前面讨论过,MIDI消息以状态字节开始,后面是0、1或2字节的数据。在dwMessage中,状态字节是最不重要的,第一个数据字节次之,第二个数据字节再次之,最重要的字节是0。
例如,要在MIDI通道5上以0x7F的速度演奏中音C(音符是0x3C),则需要3字节的NoteOn消息:
0x95 0x3C 0x7F
midiOutShortMsg的参数dwMessage等于0x007F3C95。
三个基础的MIDI消息是ProgramChange(可为某一特定通道而改变乐器声音)、Note On和NoteOff。打开一个MIDI输出设备后,应该从一条ProgramChange消息开始,然后发送相同数量的Note On和Note Off消息。
当您一直演奏您想演奏的音乐时,您可以重置MIDI输出设备以确保关闭所有的音符:
midiOutReset (hMidiOut) ;
然后关闭设备:
midiOutClose (hMidiOut) ;
使用低阶的MIDI输出API时,midiOutOpen、midiOutShortMsg、midiOutReset和midiOutClose是您需要的四个基础函数。
现在让我们演奏一段音乐。BACHTOCC,如程序22-8所示,演奏了J. S.Bach着名的风琴演奏的D小调《Toccata andFugue》中托卡塔部分的第一小节。
程序22-8 BACHTOCCBACHTOCC.C /*----------------------------------------------------------------------------- BACHTOCC.C -- Bach Toccata in D Minor (First Bar)(c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("BachTocc") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; 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 = GetStockObject (WHITE_BRUSH) ; 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, TEXT ("Bach Toccata in D Minor (First Bar)"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } DWORD MidiOutMessage ( HMIDIOUT hMidi, int iStatus, int iChannel, int iData1, int iData2) { DWORD dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ; return midiOutShortMsg (hMidi, dwMessage) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static struct { int iDur ; int iNote [2] ; } noteseq [] = { 110, 69, 81, 110, 67, 79, 990, 69, 81, 220, -1, -1, 110, 67, 79, 110, 65, 77, 110, 64, 76, 110, 62, 74, 220, 61, 73, 440, 62, 74, 1980, -1, -1, 110, 57, 69, 110, 55, 67, 990, 57, 69, 220, -1, -1, 220, 52, 64, 220, 53, 65, 220, 49, 61, 440, 50, 62, 1980, -1, -1 } ; static HMIDIOUT hMidiOut ; static int iIndex ; int i ; switch (message) { case WM_CREATE: // Open MIDIMAPPER device if (midiOutOpen (&hMidiOut, MIDIMAPPER, 0, 0, 0)) { MessageBeep (MB_ICONEXCLAMATION) ;MessageBox( hwnd, TEXT ("Cannot open MIDI output device!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return -1 ; } // Send Program Change messages for "Church Organ" MidiOutMessage (hMidiOut, 0xC0, 0, 19, 0) ; MidiOutMessage (hMidiOut, 0xC0, 12, 19, 0) ; SetTimer (hwnd, ID_TIMER, 1000, NULL) ; return 0 ; case WM_TIMER: // Loop for 2-note polyphony for (i = 0 ; i < 2 ; i++) { // Note Off messages for previous note if (iIndex != 0 && noteseq[iIndex - 1].iNote[i] != -1){MidiOutMessage (hMidiOut, 0x80, 0, noteseq[iIndex - 1].iNote[i], 0) ; MidiOutMessage (hMidiOut, 0x80, 12, noteseq[iIndex - 1].iNote[i], 0) ;}// Note On messages for new noteif (iIndex != sizeof (noteseq) / sizeof (noteseq[0]) &¬eseq[iIndex].iNote[i] != -1) { MidiOutMessage (hMidiOut, 0x90, 0, noteseq[iIndex].iNote[i], 127) ; MidiOutMessage (hMidiOut, 0x90, 12,noteseq[iIndex].iNote[i], 127) ; } } if (iIndex != sizeof (noteseq) / sizeof (noteseq[0])) { SetTimer (hwnd, ID_TIMER, noteseq[iIndex++].iDur - 1, NULL) ; } else { KillTimer (hwnd, ID_TIMER) ; DestroyWindow (hwnd) ; } return 0 ; case WM_DESTROY: midiOutReset (hMidiOut) ; midiOutClose (hMidiOut) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图22-1显示了Bach的D小调Toccata的第一小节。
图22-1 Bach的D小调Toccata and Fugue的第一小节 |
在这里要做的就是把音乐转换成一系列的数值-基本键号和定时信息,其中定时信息表示发送NoteOn(对应于风琴键按下)和NoteOff(释放键)消息的时间。由于风琴键盘对速度不敏感,所以我们用相同的速度来演奏所有的音符。另外一个简化是忽略断奏(即,在连续的音符之间留下一个很短的停顿,以达到尖硬的效果)和连奏(在连续的音符之间有更圆润的重迭)之间的区别。我们假定一个音符结束后面紧接着下一个音符开始。
如果看得懂乐谱,那么您就会注意到托卡塔曲以两个平行的八度音阶开始。因此BACHTOCC建立了一个数据结构noteseq来储存一系列的音符持续时间以及两个键号。不幸的是,音乐持续进入第二小节就需要更特殊的方法来储存此信息。我将四分音符的持续时间定义为1760毫秒,也就是说,八分音符(在音符或者休止符上有一个符尾)的持续时间是880毫秒,十六分音符(两个符尾)是440毫秒,三十二分音符(三个符尾)是220毫秒,六十四分音符(四个符尾)是110毫秒。
这第一小节中有两个波音-一个在第一个音符处,另一个在小节的中间。这在乐谱上用带一条短竖线的曲线表示。在结构复杂的乐曲中,波音符号表示此音符实际应演奏为三个音符-标出的音符、比它低一个全音的音符,然后还是标出的音符。前两个音符演奏得要快,第三个音符要持续剩余的时间。例如,第一个音符是带波音的A,则应演奏为A、G、A。我将波音的前两个音符定义为六十四分音符,所以每个音符都持续110毫秒。
在第一小节还有四个延长符号。乐谱上表示为中间带点的半圆形。延长符号表示该音符在演奏时所持续的时间比标记的时间要长,通常由演奏者决定具体的时间。我对于延长符号延长了50%的时间。
可以看到,即使是转换一小段看来简单直接的乐曲,例如D小调《Toccata》的开头,也并不是件容易的事!
noteseq结构数组包含了这一小节中平行的音符和休止符的三个数字。音符持续时间的后面是用于平行八度音阶的两个MIDI键号。例如,第一个音符是A,持续时间是110毫秒。因为中音C的MIDI键号是60,所以中音C上面的A的键号是69,比A高一个八度音阶的键号是81。因此,noteseq数组的前三个数是110、69和81。我用音符值-1表示休止符。
WM_CREATE消息处理期间,BACHTOCC设定一个Windows定时器用于定时1000毫秒-表示乐曲从第1秒开始演奏-然后用MIDIMAPPER设备ID呼叫midiOutOpen。
BACHTOCC只需要一种乐器(风琴)的声音,所以只需要一个通道。为了简化MIDI消息的发送,BACHTOCC中还定义了一个小函数MidiOutMessage。此函数接收MIDI输出句柄、状态字节、信道句柄和两个字节数据。其功能是把这些数字打包到一条32位的消息并呼叫midiOutShortMsg。
在WM_CREATE消息处理程序的后期,BACHTOCC发送一条ProgramChange消息来选择「教堂风琴」的声音。在GeneralMIDI声音配置中,教堂风琴声音在ProgramChange消息中用数字字节19表示。实际演奏的音符出现在WM_TIMER消息处理期间。用循环来处理两个音符的多音。如果前一个音符还在演奏,BACHTOCC就为该音符发送NoteOff消息。然后,如果下一个音符不是休止符,则向通道0和12发送NoteOn消息。随后,重置Windows定时器,使其与noteseq结构中音符的持续时间一致。
音乐演奏完后,BACHTOCC删除窗口。在WM_DESTROY消息处理期间,程序呼叫midiOutReset和midiOutClose,然后终止程序。
尽管BACHTOCC合理地处理和计算声音(即使还不完全像真人演奏风琴),但一般情况下用Windows定时器按这种方式来演奏音乐并不管用。问题在于Windows定时器是依据PC的系统时钟,其分辨率不能满足音乐的要求。而且,Windows定时器不是同步的。这样,如果其它程序正忙于执行,则获得WM_TIMER消息就会有轻微的延迟。如果程序不能立即处理这些消息,就会放弃WM_TIMER消息,这时的声音听起来一团糟。
因此,当BACHTOCC显示了如何呼叫低阶MIDI输出函数时,使用Windows定时器显然不适合精确的音乐创作。所以,Windows还提供了一系列附加的定时器函数,使用低阶的MIDI输出函数时可以利用这些函数。这些函数的前缀为time,您可以利用它们将定时器的分辨率设定到最小1毫秒。我将在本章结尾的DRUM程序向您展示使用这些函数的方法。
通过键盘演奏MIDI合成器
因为大多数PC使用者可能都没有连结在机器上的MIDI键盘,所以可以用每个人都有的键盘(上面全部的字母键和数据键)来代替。程序22-9所示的程序KBMIDI允许您用PC键盘来演奏电子音乐合成器-不管是连结在声卡上的,还是挂接在MIDIOut埠的外部合成器。KBMIDI让您完全控制MIDI输出设备(即内部或外部的合成器)、MIDI通道和乐器声音。除了演奏时的趣味性以外,我还发现此程序对于开发Windows如何实作MIDI支持很有用。
程序22-9 KBMIDIKBMIDI.C /*-------------------------------------------------------------------------- KBMIDI.C -- Keyboard MIDI Player(c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> // Defines for Menu IDs // -------------------- #defineIDM_OPEN 0x100 #defineIDM_CLOSE 0x101 #defineIDM_DEVICE0x200 #defineIDM_CHANNEL 0x300 #defineIDM_VOICE 0x400 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName [] = TEXT ("KBMidi") ; HMIDIOUT hMidiOut ; int iDevice = MIDIMAPPER, iChannel = 0, iVoice = 0, iVelocity = 64 ; intcxCaps, cyChar, xOffset, yOffset ; // Structures and data for showing families and instruments on menu // -------------------------------------------------------------------- typedef struct { TCHAR * szInst ; intiVoice ; } INSTRUMENT ; typedef struct { TCHAR * szFam ; INSTRUMENT inst [8] ; } FAMILY ; FAMILY fam [16] = { TEXT ("Piano"), TEXT ("Acoustic Grand Piano"), 0, TEXT ("Bright Acoustic Piano"),1, TEXT ("Electric Grand Piano"), 2, TEXT ("Honky-tonk Piano"), 3, TEXT ("Rhodes Piano"), 4, TEXT ("Chorused Piano"), 5, TEXT ("Harpsichord"), 6, TEXT ("Clavinet"), 7, TEXT ("Chromatic Percussion"), TEXT ("Celesta"), 8, TEXT ("Glockenspiel"), 9, TEXT ("Music Box"), 10, TEXT ("Vibraphone"), 11, TEXT ("Marimba"), 12, TEXT ("Xylophone"), 13, TEXT ("Tubular Bells"), 14, TEXT ("Dulcimer"),15, TEXT ("Organ"), TEXT ("Hammond Organ"), 16, TEXT ("Percussive Organ"),17, TEXT ("Rock Organ"), 18, TEXT ("Church Organ"),19, TEXT ("Reed Organ"), 20, TEXT ("Accordian"), 21, TEXT ("Harmonica"), 22, TEXT ("Tango Accordian"), 23, TEXT ("Guitar"), TEXT ("Acoustic Guitar (nylon)"), 24, TEXT ("Acoustic Guitar (steel)"), 25,TEXT ("Electric Guitar (jazz)"), 26, TEXT ("Electric Guitar (clean)"), 27, TEXT ("Electric Guitar (muted)"), 28, TEXT ("Overdriven Guitar"), 29, TEXT ("Distortion Guitar"), 30, TEXT ("Guitar Harmonics"),31, TEXT ("Bass"), TEXT ("Acoustic Bass"), 32, TEXT ("Electric Bass (finger)"), 33, TEXT ("Electric Bass (pick)"),34, TEXT ("Fretless Bass"), 35, TEXT ("Slap Bass 1"), 36, TEXT ("Slap Bass 2"), 37, TEXT ("Synth Bass 1"),38, TEXT ("Synth Bass 2"),39, TEXT ("Strings"), TEXT ("Violin"), 40, TEXT ("Viola"), 41, TEXT ("Cello"), 42, TEXT ("Contrabass"), 43, TEXT ("Tremolo Strings"), 44, TEXT ("Pizzicato Strings"), 45, TEXT ("Orchestral Harp"), 46, TEXT ("Timpani"), 47, TEXT ("Ensemble"), TEXT ("String Ensemble 1"), 48, TEXT ("String Ensemble 2"), 49, TEXT ("Synth Strings 1"),50, TEXT ("Synth Strings 2"), 51, TEXT ("Choir Aahs"), 52, TEXT ("Voice Oohs"), 53, TEXT ("Synth Voice"), 54, TEXT ("Orchestra Hit"), 55, TEXT ("Brass"), TEXT ("Trumpet"), 56, TEXT ("Trombone"),57, TEXT ("Tuba"),58, TEXT ("Muted Trumpet"), 59, TEXT ("French Horn"), 60, TEXT ("Brass Section"), 61, TEXT ("Synth Brass 1"), 62, TEXT ("Synth Brass 2"), 63, TEXT ("Reed"), TEXT ("Soprano Sax"), 64, TEXT ("Alto Sax"),65, TEXT ("Tenor Sax"), 66, TEXT ("Baritone Sax"),67, TEXT ("Oboe"),68, TEXT ("English Horn"),69, TEXT ("Bassoon"), 70, TEXT ("Clarinet"),71, TEXT ("Pipe"), TEXT ("Piccolo"), 72, TEXT ("Flute "), 73, TEXT ("Recorder"),74, TEXT ("Pan Flute"), 75, TEXT ("Bottle Blow"), 76, TEXT ("Shakuhachi"), 77, TEXT ("Whistle"), 78, TEXT ("Ocarina"), 79, TEXT ("Synth Lead"), TEXT ("Lead 1 (square)"), 80, TEXT ("Lead 2 (sawtooth)"), 81, TEXT ("Lead 3 (caliope lead)"), 82, TEXT ("Lead 4 (chiff lead)"), 83, TEXT ("Lead 5 (charang)"),84, TEXT ("Lead 6 (voice)"), 85, TEXT ("Lead 7 (fifths)"), 86, TEXT ("Lead 8 (brass + lead)"), 87, TEXT ("Synth Pad"), TEXT ("Pad 1 (new age)"), 88, TEXT ("Pad 2 (warm)"),89, TEXT ("Pad 3 (polysynth)"), 90, TEXT ("Pad 4 (choir)"), 91, TEXT ("Pad 5 (bowed)"), 92, TEXT ("Pad 6 (metallic)"),93, TEXT ("Pad 7 (halo)"), 94, TEXT ("Pad 8 (sweep)"), 95, TEXT ("Synth Effects"), TEXT ("FX 1 (rain)"), 96, TEXT ("FX 2 (soundtrack)"), 97, TEXT ("FX 3 (crystal)"), 98, TEXT ("FX 4 (atmosphere)"), 99, TEXT ("FX 5 (brightness)"), 100, TEXT ("FX 6 (goblins)"), 101, TEXT ("FX 7 (echoes)"), 102, TEXT ("FX 8 (sci-fi)"), 103, TEXT ("Ethnic"), TEXT ("Sitar"), 104, TEXT ("Banjo"), 105, TEXT ("Shamisen"), 106, TEXT ("Koto"), 107, TEXT ("Kalimba"),108, TEXT ("Bagpipe"),109, TEXT ("Fiddle"), 110, TEXT ("Shanai"), 111, TEXT ("Percussive"), TEXT ("Tinkle Bell"),112, TEXT ("Agogo"), 113, TEXT ("Steel Drums"),114, TEXT ("Woodblock"), 115, TEXT ("Taiko Drum"),116, TEXT ("Melodic Tom"),117, TEXT ("Synth Drum"),118, TEXT ("Reverse Cymbal"), 119, TEXT ("Sound Effects"), TEXT ("Guitar Fret Noise"), 120, TEXT ("Breath Noise"), 121, TEXT ("Seashore"), 122, TEXT ("Bird Tweet"), 123, TEXT ("Telephone Ring"), 124, TEXT ("Helicopter"), 125, TEXT ("Applause"), 126, TEXT ("Gunshot"),127 } ; // Data for translating scan codes to octaves and notes // ---------------------------------------------------- #define NUMSCANS(sizeof key / sizeof key[0]) struct { int iOctave ; int iNote ; int yPos ; int xPos ; TCHAR * szKey ; } key [] = { // Scan Char Oct Note // ---- ---- --- ---- -1,-1,1, -1,NULL, // 0 None -1,-1,-1,-1,NULL, // 1 Esc -1,-1,0, 0, TEXT (""),// 2 1 5, 1, 0, 2, TEXT ("C#"), // 3 2 5 C# 5, 3, 0, 4, TEXT ("D#"), // 4 3 5 D# -1,-1,0, 6, TEXT (""),// 5 4 5, 6, 0, 8, TEXT ("F#"), // 6 5 5 F# 5, 8, 0, 10,TEXT ("G#"), // 7 6 5 G# 5, 10,0, 12,TEXT ("A#"), // 8 7 5 A# -1,-1,0, 14,TEXT (""),// 9 8 6, 1, 0, 16,TEXT ("C#"), // 10 9 6 C# 6, 3, 0, 18,TEXT ("D#"), // 11 0 6 D# -1,-1,0, 20,TEXT (""),// 12 - 6, 6, 0, 22,TEXT ("F#"), // 13 = 6 F# -1,-1,-1,-1,NULL, // 14 Back -1,-1,-1,-1,NULL, // 15 Tab 5, 0, 1, 1, TEXT ("C"), // 16 q 5 C 5, 2, 1, 3, TEXT ("D"), // 17 w 5 D 5, 4, 1, 5, TEXT ("E"), // 18 e 5E 5, 5, 1, 7, TEXT ("F"), // 19 r 5 F 5, 7, 1, 9, TEXT ("G"), // 20 t 5 G 5, 9, 1, 11,TEXT ("A"), // 21 y 5A 5, 11,1, 13,TEXT ("B"), // 22 u 5B 6, 0, 1, 15,TEXT ("C"), // 23 i 6 C 6, 2, 1, 17,TEXT ("D"), // 24 o 6D 6, 4, 1, 19,TEXT ("E"), // 25 p 6E 6, 5, 1, 21,TEXT ("F"), // 26 [ 6F 6, 7, 1, 23,TEXT ("G"), // 27 ] 6G -1,-1,-1,-1,NULL, // 28Ent -1,-1,-1,-1,NULL, // 29Ctrl 3, 8, 2, 2, TEXT ("G#"), // 30 a 3G# 3, 10,2, 4, TEXT ("A#"), // 31 s 3A# -1,-1,2, 6, TEXT (""),// 32 d 4, 1, 2, 8, TEXT ("C#"), // 33 f 4C# 4, 3, 2, 10,TEXT ("D#"), // 34 g 4D# -1,-1,2, 12,TEXT (""),// 35 h 4, 6, 2, 14,TEXT ("F#"), // 36 j 4F# 4, 8, 2, 16,TEXT ("G#"), // 37 k 4G# 4, 10,2, 18,TEXT ("A#"), // 38 l 4A# -1,-1,2, 20,TEXT (""),// 39 ; 5, 1, 2, 22,TEXT ("C#"), // 40 ' 5C# -1,-1,-1,-1,NULL, // 41 ` -1,-1,-1,-1,NULL, // 42Shift -1,-1,-1,-1,NULL, // 43 \ (not line continuation) 3, 9, 3, 3, TEXT ("A"), // 44 z 3 A 3, 11,3, 5, TEXT ("B"), // 45 x 3B 4, 0, 3, 7, TEXT ("C"), // 46 c 4C 4, 2, 3, 9, TEXT ("D"), // 47 v 4 D 4, 4, 3, 11,TEXT ("E"), // 48 b 4E 4, 5, 3, 13,TEXT ("F"), // 49n 4F 4, 7, 3, 15,TEXT ("G"), // 50 m 4G 4, 9, 3, 17,TEXT ("A"), // 51 , 4 A 4, 11,3, 19,TEXT ("B"), // 52 . 4B 5, 0, 3, 21,TEXT ("C")// 53 / 5C } ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { MSG msg; HWNDhwnd ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; 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 = GetStockObject (WHITE_BRUSH) ; 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, TEXT ("Keyboard MIDI Player"), WS_OVERLAPPEDWINDOW | WS_HSCROLL | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; if (!hwnd) return 0 ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd); while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } // Create the program's menu (called from WndProc, WM_CREATE) // -------------------------------------------------------------------- HMENU CreateTheMenu (int iNumDevs) { TCHARszBuffer [32] ; HMENUhMenu, hMenuPopup, hMenuSubPopup ; int i, iFam, iIns ; MIDIOUTCAPS moc ; hMenu = CreateMenu () ; // Create "On/Off" popup menu hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_OPEN, TEXT ("&Open")) ; AppendMenu (hMenuPopup, MF_STRING | MF_CHECKED, IDM_CLOSE, TEXT ("&Closed")) ; AppendMenu (hMenu,MF_STRING | MF_POPUP, (UINT) hMenuPopup, TEXT ("&Status")) ; // Create "Device" popup menu hMenuPopup = CreateMenu () ; // Put MIDI Mapper on menu if it's installed if (!midiOutGetDevCaps (MIDIMAPPER, &moc, sizeof (moc))) AppendMenu (hMenuPopup, MF_STRING, IDM_DEVICE + (int) MIDIMAPPER,moc.szPname) ; else iDevice = 0 ; // Add the rest of the MIDI devices for (i = 0 ; i < iNumDevs ; i++) { midiOutGetDevCaps (i, &moc, sizeof (moc)) ; AppendMenu (hMenuPopup, MF_STRING, IDM_DEVICE + i, moc.szPname) ; } CheckMenuItem (hMenuPopup, 0, MF_BYPOSITION | MF_CHECKED) ; AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup,TEXT ("&Device")) ; // Create "Channel" popup menu hMenuPopup = CreateMenu () ; for (i = 0 ; i < 16 ; i++) { wsprintf (szBuffer, TEXT ("%d"), i + 1) ; AppendMenu (hMenuPopup, MF_STRING | (i ? MF_UNCHECKED : MF_CHECKED), IDM_CHANNEL + i, szBuffer) ; } AppendMenu (hMenu, MF_STRING | MF_POPUP, (UINT) hMenuPopup,TEXT ("&Channel")) ; // Create "Voice" popup menu hMenuPopup = CreateMenu () ; for (iFam = 0 ; iFam < 16 ; iFam++) { hMenuSubPopup = CreateMenu () ; for (iIns = 0 ; iIns < 8 ; iIns++) { wsprintf (szBuffer, TEXT ("&%d.\t%s"), iIns + 1,fam[iFam].inst[iIns].szInst) ; AppendMenu (hMenuSubPopup, MF_STRING | (fam[iFam].inst[iIns].iVoice ? MF_UNCHECKED : MF_CHECKED), fam[iFam].inst[iIns].iVoice + IDM_VOICE, szBuffer) ; } wsprintf (szBuffer, TEXT ("&%c.\t%s"), 'A' + iFam, fam[iFam].szFam) ; AppendMenu (hMenuPopup, MF_STRING | MF_POPUP, (UINT) hMenuSubPopup, szBuffer) ; } AppendMenu (hMenu,MF_STRING | MF_POPUP, (UINT) hMenuPopup, TEXT ("&Voice")) ; return hMenu ; } // Routines for simplifying MIDI output // ------------------------------------ DWORD MidiOutMessage ( HMIDIOUT hMidi, int iStatus, int iChannel,int iData1, int iData2) { DWORD dwMessage ; dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ; return midiOutShortMsg (hMidi, dwMessage) ; } DWORD MidiNoteOff (HMIDIOUT hMidi, int iChannel, int iOct, int iNote, int iVel) { return MidiOutMessage (hMidi, 0x080, iChannel, 12 * iOct + iNote, iVel) ; } DWORD MidiNoteOn (HMIDIOUT hMidi, int iChannel, int iOct, int iNote, int iVel) { return MidiOutMessage ( hMidi, 0x090, iChannel, 12 * iOct + iNote, iVel) ; } DWORD MidiSetPatch (HMIDIOUT hMidi, int iChannel, int iVoice) { return MidiOutMessage (hMidi, 0x0C0, iChannel, iVoice, 0) ; } DWORD MidiPitchBend (HMIDIOUT hMidi, int iChannel, int iBend) { return MidiOutMessage (hMidi, 0x0E0, iChannel, iBend & 0x7F, iBend >> 7) ; } // Draw a single key on window // ---------------------------------- VOID DrawKey (HDC hdc, int iScanCode, BOOL fInvert) { RECT rc ; rc.left = 3 * cxCaps * key[iScanCode].xPos / 2 + xOffset ; rc.top= 3 * cyChar * key[iScanCode].yPos / 2 + yOffset ; rc.right = rc.left + 3 * cxCaps ; rc.bottom = rc.top + 3 * cyChar / 2 ; SetTextColor (hdc, fInvert ? 0x00FFFFFFul : 0x00000000ul) ; SetBkColor(hdc, fInvert ? 0x00000000ul : 0x00FFFFFFul) ; FillRect (hdc, &rc, GetStockObject (fInvert ? BLACK_BRUSH : WHITE_BRUSH)) ; DrawText (hdc, key[iScanCode].szKey, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; FrameRect (hdc, &rc, GetStockObject (BLACK_BRUSH)) ; } // Process a Key Up or Key Down message // ------------------------------------ VOID ProcessKey (HDC hdc, UINT message, LPARAM lParam) { int iScanCode, iOctave, iNote ; iScanCode = 0x0FF & HIWORD (lParam) ; if (iScanCode >= NUMSCANS) // No scan codes over 53 return ; if ((iOctave = key[iScanCode].iOctave) == -1) // Non-music key return ; if (GetKeyState (VK_SHIFT) < 0)iOctave += 0x20000000 & lParam ? 2 : 1 ; if (GetKeyState (VK_CONTROL) < 0) iOctave -= 0x20000000 & lParam ? 2 : 1 ; iNote = key[iScanCode].iNote ; if (message == WM_KEYUP) // For key up { MidiNoteOff (hMidiOut, iChannel, iOctave, iNote, 0) ; // Note offDrawKey (hdc, iScanCode, FALSE) ; return ; } if (0x40000000 & lParam) // ignore typematics return ; MidiNoteOn (hMidiOut, iChannel, iOctave, iNote, iVelocity) ; // Note on DrawKey (hdc, iScanCode, TRUE) ; // Draw the inverted key } // Window Procedure // --------------------- LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL bOpened = FALSE ; HDC hdc ; HMENUhMenu ; int i, iNumDevs, iPitchBend, cxClient, cyClient ; MIDIOUTCAPS moc ; PAINTSTRUCT ps ; SIZE size ; TCHARszBuffer [16] ; switch (message) { case WM_CREATE: // Get size of capital letters in system font hdc = GetDC (hwnd) ; GetTextExtentPoint (hdc, TEXT ("M"), 1, &size) ; cxCaps = size.cx ; cyChar = size.cy ; ReleaseDC (hwnd, hdc) ; // Initialize "Volume" scroll bar SetScrollRange(hwnd, SB_HORZ, 1, 127, FALSE) ; SetScrollPos (hwnd, SB_HORZ, iVelocity, TRUE) ; // Initialize "Pitch Bend" scroll bar SetScrollRange(hwnd, SB_VERT, 0, 16383, FALSE) ; SetScrollPos (hwnd, SB_VERT, 8192, TRUE) ; // Get number of MIDI output devices and set up menu if (0 == (iNumDevs = midiOutGetNumDevs ())) { MessageBeep (MB_ICONSTOP) ; MessageBox ( hwnd, TEXT ("No MIDI output devices!"), szAppName, MB_OK | MB_ICONSTOP) ; return -1 ; } SetMenu (hwnd, CreateTheMenu (iNumDevs)) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; xOffset = (cxClient - 25 * 3 * cxCaps / 2) / 2 ; yOffset = (cyClient - 11 * cyChar) / 2 + 5 * cyChar ; return 0 ; case WM_COMMAND: hMenu = GetMenu (hwnd) ; // "Open" menu command if (LOWORD (wParam) == IDM_OPEN && !bOpened) { if (midiOutOpen (&hMidiOut, iDevice, 0, 0, 0)) { MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Cannot open MIDI device"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; } else { CheckMenuItem (hMenu, IDM_OPEN, MF_CHECKED) ;CheckMenuItem (hMenu, IDM_CLOSE, MF_UNCHECKED) ; MidiSetPatch (hMidiOut, iChannel, iVoice) ; bOpened = TRUE ; } } // "Close" menu command else if (LOWORD (wParam) == IDM_CLOSE && bOpened) {CheckMenuItem (hMenu, IDM_OPEN, MF_UNCHECKED) ; CheckMenuItem (hMenu, IDM_CLOSE, MF_CHECKED) ; // Turn all keys off and close device for (i = 0 ; i < 16 ; i++) MidiOutMessage (hMidiOut, 0xB0, i, 123, 0) ; midiOutClose (hMidiOut) ; bOpened = FALSE ; } // Change MIDI "Device" menu command else if ( LOWORD (wParam) >= IDM_DEVICE - 1 && LOWORD (wParam) < IDM_CHANNEL) { CheckMenuItem (hMenu, IDM_DEVICE + iDevice, MF_UNCHECKED) ; iDevice = LOWORD (wParam) - IDM_DEVICE ; CheckMenuItem (hMenu, IDM_DEVICE + iDevice, MF_CHECKED) ; // Close and reopen MIDI device if (bOpened) {SendMessage (hwnd, WM_COMMAND, IDM_CLOSE, 0L) ; SendMessage (hwnd, WM_COMMAND, IDM_OPEN, 0L) ; } } // Change MIDI "Channel" menu command else if ( LOWORD (wParam) >= IDM_CHANNEL &&LOWORD (wParam) < IDM_VOICE) { CheckMenuItem (hMenu, IDM_CHANNEL + iChannel, MF_UNCHECKED); iChannel = LOWORD (wParam) - IDM_CHANNEL ; CheckMenuItem (hMenu, IDM_CHANNEL + iChannel, MF_CHECKED) ; if (bOpened) MidiSetPatch (hMidiOut, iChannel, iVoice) ; } // Change MIDI "Voice" menu command else if (LOWORD (wParam) >= IDM_VOICE) { CheckMenuItem (hMenu, IDM_VOICE + iVoice, MF_UNCHECKED) ; iVoice = LOWORD (wParam) - IDM_VOICE ; CheckMenuItem (hMenu, IDM_VOICE + iVoice, MF_CHECKED) ; if (bOpened)MidiSetPatch (hMidiOut, iChannel, iVoice) ; } InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; // Process a Key Up or Key Down message case WM_KEYUP: case WM_KEYDOWN: hdc = GetDC (hwnd) ; if (bOpened) ProcessKey (hdc, message, lParam) ; ReleaseDC (hwnd, hdc) ; return 0 ; // For Escape, turn off all notes and repaint case WM_CHAR: if (bOpened && wParam == 27) { for (i = 0 ; i < 16 ; i++) MidiOutMessage (hMidiOut, 0xB0, i, 123, 0) ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; // Horizontal scroll: Velocity case WM_HSCROLL: switch (LOWORD (wParam)) {case SB_LINEUP: iVelocity -= 1 ; break ; case SB_LINEDOWN:iVelocity += 1 ; break ; case SB_PAGEUP: iVelocity -= 8 ; break ; case SB_PAGEDOWN:iVelocity += 8 ; break ; case SB_THUMBPOSITION:iVelocity = HIWORD (wParam) ; break ; default:return 0 ; } iVelocity = max (1, min (iVelocity, 127)) ; SetScrollPos (hwnd, SB_HORZ, iVelocity, TRUE) ; return 0 ; // Vertical scroll: Pitch Bend case WM_VSCROLL: switch (LOWORD (wParam)) { case SB_THUMBTRACK: iPitchBend = 16383 - HIWORD (wParam) ; break ; case SB_THUMBPOSITION: iPitchBend = 8191 ; break ; default:return 0 ; } iPitchBend = max (0, min (iPitchBend, 16383)) ; SetScrollPos (hwnd, SB_VERT, 16383 - iPitchBend, TRUE) ; if (bOpened)MidiPitchBend (hMidiOut, iChannel, iPitchBend) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; for (i = 0 ; i < NUMSCANS ; i++) if (key[i].xPos != -1) DrawKey (hdc, i, FALSE) ; midiOutGetDevCaps (iDevice, &moc, sizeof (MIDIOUTCAPS)) ;wsprintf (szBuffer, TEXT ("Channel %i"), iChannel + 1) ; TextOut ( hdc, cxCaps, 1 * cyChar, Opened ? TEXT ("Open") : TEXT ("Closed"), bOpened ? 4 : 6) ; TextOut ( hdc, cxCaps, 2 * cyChar, moc.szPname, lstrlen (moc.szPname)) ; TextOut (hdc, cxCaps, 3 * cyChar, szBuffer, lstrlen (szBuffer)) ; TextOut(hdc, cxCaps, 4 * cyChar, fam[iVoice / 8].inst[iVoice % 8].szInst, lstrlen (fam[iVoice / 8].inst[iVoice % 8].szInst)) ; EndPaint (hwnd, &ps) ; return 0 ;case WM_DESTROY : SendMessage (hwnd, WM_COMMAND, IDM_CLOSE, 0L) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
执行KBMIDI时,窗口显示了键盘上的键与传统钢琴或风琴按键的对应方式。左下角的Z键以110Hz的频率演奏A。键盘的最下行,右边是中音C,倒数第二行为其升音或降音。上面两行键继续按此规律变化,从中音C到G#。这样,整个范围是三个八度音阶。另外,分别按Shift键和Ctrl键可使整个音域上升或下降1个八度音阶,这样有效的音域就是5个八度音阶。
不过,如果立即开始演奏,那么您将听不到任何声音。您必须先从「Status」菜单中选择「Open」,打开一个MIDI输出设备。如果埠打开成功,则按下一个键就向合成器发送一条MIDINote On消息,释放键则产生一条NoteOff消息。取决于键盘的按键特性,您可以同时演奏几个音符。
从「Status」菜单里选择「Close」来关闭MIDI设备。这对于需要在不终止KBMIDI程序的情况下执行Windows下的其它MIDI软件来说是很方便的。
「Device」菜单列出了已安装的MIDI输出设备,这些设备通过呼叫midiOutGetDevCaps函数获得。其中有些设备可能是MIDIOut埠连结的实际存在或不存在的外部合成器。列表还包括MIDIMapper设备。这是从「控制台」的「多媒体」中选择的MIDI合成器。
「Channel」菜单用来选择从1到16的MIDI通道,内定状态下选择通道1。KBMIDI程序产生的所有MIDI消息都发送到所选的通道。
KBMIDI最后一个菜单项是「Voice」,它是一个双层菜单,用于选择128种乐器声音,这些声音在GeneralMIDI规范中定义并在Windows中实作。这128种乐器声音分为16乐器组,每个乐器组有8种乐器。由于不同的MIDI键号对应于不同的泛音,所以这128种乐器声音也称为有旋律的声音。
GeneralMIDI中还定义了大量无旋律的打击乐器。要演奏打击乐器,可以从「Channel」菜单选择通道10,还可以从「Voice」菜单选择第一种乐器声音(「AcousticGrandPiano」)。这样,按不同的键就可以得到不同打击乐器的声音。从MIDI键号35(低于中音C两个八度音阶的B)到81(高于中音C近两个八度音阶的A),共有47种不同的打击乐器声音。在下面的DRUM程序中就利用了打击乐器通道。
KBMIDI程序有水平和垂直滚动条。由于PC键盘对按键速度不敏感,所以用水平滚动条来控制音符速度。一般来说,这与演奏音符的音量一致。设定完水平滚动条以后,所有的NoteOn消息都将使用这个速度。
垂直滚动条将产生一条称为「PitchBend」的MIDI消息。要使用此特性,请按下一个或多个键,然后用鼠标拖动滚动条。向上拖动滚动条音符频率将上升,向下拖动则频率下降。释放滚动条后将恢复正常的基音。
这两个滚动条要小心使用:因为拖动滚动条时,键盘消息将不进入程序的消息循环。因此,如果按下一个键后就开始拖动滚动条,然后在完成拖动之前就释放了该键,那么音符仍将发声。所以,拖动滚动条时不要按下或者释放任何键。对菜单也有类似的规则:按着键时不要进行菜单选择。另外,在按下与释放某个键期间,不要用Ctrl或Shift键来改变八度音阶。
如果一个或者多个音符出现「粘滞现象」,即释放后继续发声,那么请按下Esc键。按下此键将通过向MIDI合成器的16个通道发送16条AllNotes Off消息,来关闭声音。
KBMIDI没有资源描述档,而是通过搜索来建立的菜单。设备名称从midiOutGetDevCaps函数获得,乐器种类和名称则储存在程序的一个大数据结构中。
KBMIDI定义了几个小函数来简化MIDI消息。除了PitchBend消息以外,其它消息都在前面讨论过了。PitchBend消息用两个7位值组成一个14位的音调弯曲等级:0到0x1FFF之间的值降低基音,0x2001到0x3FFF之间的值升高基音。
从「Status」菜单选择「Open」时,KBMIDI为选择的设备呼叫midiOutOpen;如呼叫成功,则呼叫MidiSetPatch函数。设备改变时,KBMIDI必须关闭前一个设备,必要时再打开新设备。当改变MIDI设备、MIDI通道、乐器声音时,KBMIDI也必须呼叫MidiSetPatch。
KBMIDI通过处理WM_KEYUP消息和WM_KEYDOWN消息来控制音符的发音。KBMIDI中用一个数组把键盘扫描码映像成八度音阶和音符。例如,美国英语键盘上Z键的扫描码是44,数组将其标记为八度音阶是3,音符是9(即A)。在KBMIDI的MidiNoteOn函数里,这些组合成了MIDI键号45(即12乘以3再加上9)。此数据结构也用于在窗口中画出键-每个键都有特定的水平和垂直位置,以及显示在矩形中的文字字符串。
水平滚动条的处理是很直接的:所有需要做的就是储存新的速度级并设定新的滚动条的位置。但是处理垂直滚动条以控制音调弯曲的操作稍有一点特殊,它处理的滚动条命令只有两个:用鼠标拖动滚动条时发生的SB_THUMBTRACK,以及释放滚动条时的SB_THUMBPOSITION。处理SB_THUMBPOSITION命令时,KBMIDI将滚动条位置设定为中间等级,并呼叫MidiPitchBend,其中参数值是8192。
MIDI击鼓器
有些打击乐器,如木琴或定音鼓,是「有旋律的」或「半音阶的」,因为它们可以用不同的音阶演奏乐曲。木琴用木板来对应不同的音阶,定音鼓也可以演奏曲调。这两种乐器及其它的有旋律的打击乐器都可以在KBMIDI的「Voice」菜单里选择。
但是,其它许多打击乐器都没有旋律,它们不能调音,而且通常含有太多的噪音,以致不能与某个基音相联系。在「GeneralMIDI」规范中,这些没旋律的打击乐器声在通道10有效。不同的键号对应47种不同的打击乐器。
DRUM程序,如程序22-10所示,是一个计算机击鼓器。此程序让您用47种不同的打击乐器的声音来构造最大到32个音符的一个序列,然后在选择的速度和音量下反复演奏这个序列。
程序22-10 DRUMDRUM.C /*--------------------------------------------------------------------------- DRUM.C -- MIDI Drum Machine (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include <stdlib.h> #include <string.h> #include <math.h> #include "drumtime.h" #include "drumfile.h" #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; BOOL CALLBACK AboutProc(HWND, UINT, WPARAM, LPARAM) ; void DrawRectangle (HDC, int, int, DWORD *, DWORD *) ; void ErrorMessage (HWND, TCHAR *, TCHAR *) ; void DoCaption (HWND, TCHAR *) ; intAskAboutSave (HWND, TCHAR *) ; TCHAR * szPerc [NUM_PERC] = { TEXT ("Acoustic Bass Drum"), TEXT ("Bass Drum 1"), TEXT ("Side Stick"), TEXT ("Acoustic Snare"), TEXT ("Hand Clap"), TEXT ("Electric Snare"), TEXT ("Low Floor Tom"), TEXT ("Closed High Hat"), TEXT ("High Floor Tom"), TEXT ("Pedal High Hat"), TEXT ("Low Tom"),TEXT ("Open High Hat"), TEXT ("Low-Mid Tom"), TEXT ("High-Mid Tom"), TEXT ("Crash Cymbal 1"), TEXT ("High Tom"),TEXT ("Ride Cymbal 1"), TEXT ("Chinese Cymbal"), TEXT ("Ride Bell"), TEXT ("Tambourine"), TEXT ("Splash Cymbal"), TEXT ("Cowbell"), TEXT ("Crash Cymbal 2"), TEXT ("Vibraslap"), TEXT ("Ride Cymbal 2"), TEXT ("High Bongo"), TEXT ("Low Bongo"), TEXT ("Mute High Conga"), TEXT ("Open High Conga"), TEXT ("Low Conga"), TEXT ("High Timbale"), TEXT ("Low Timbale"), TEXT ("High Agogo"),TEXT ("Low Agogo"), TEXT ("Cabasa"),TEXT ("Maracas"), TEXT ("Short Whistle"), TEXT ("Long Whistle"), TEXT ("Short Guiro"),TEXT ("Long Guiro"), TEXT ("Claves"),TEXT ("High Wood Block"), TEXT ("Low Wood Block"), TEXT ("Mute Cuica"), TEXT ("Open Cuica"), TEXT ("Mute Triangle"), TEXT ("Open Triangle") } ; TCHAR szAppName [] = TEXT ("Drum") ; TCHAR szUntitled[] = TEXT ("(Untitled)") ; TCHAR szBuffer [80 + MAX_PATH] ; HANDLE hInst ; intcxChar, cyChar ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, NULL, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_HSCROLL | WS_VSCROLL, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,NULL, NULL, hInstance, szCmdLine) ; 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) { staticBOOL bNeedSave ; static DRUM drum ; static HMENU hMenu ; staticint iTempo = 50, iIndexLast ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; HDC hdc ; int i, x, y ; PAINTSTRUCT ps ; POINTpoint ; RECT rect ; TCHAR* szError ; switch (message) { case WM_CREATE: // Initialize DRUM structure drum.iMsecPerBeat = 100 ; drum.iVelocity= 64 ; drum.iNumBeats= 32 ; DrumSetParams (&drum) ; // Other initialization cxChar = LOWORD (GetDialogBaseUnits ()) ; cyChar = HIWORD (GetDialogBaseUnits ()) ; GetWindowRect (hwnd, &rect) ; MoveWindow (hwnd, rect.left, rect.top, 77 * cxChar, 29 * cyChar, FALSE) ; hMenu = GetMenu (hwnd) ; // Initialize "Volume" scroll bar SetScrollRange(hwnd, SB_HORZ, 1, 127, FALSE) ; SetScrollPos (hwnd, SB_HORZ, drum.iVelocity, TRUE) ; // Initialize "Tempo" scroll bar SetScrollRange(hwnd, SB_VERT, 0, 100, FALSE) ; SetScrollPos (hwnd, SB_VERT, iTempo, TRUE) ; DoCaption (hwnd, szTitleName) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_FILE_NEW: if ( bNeedSave && IDCANCEL == AskAboutSave (hwnd, szTitleName)) return 0 ; // Clear drum pattern for (i = 0 ; i < NUM_PERC ; i++){ drum.dwSeqPerc [i] = 0 ; drum.dwSeqPian [i] = 0 ; } InvalidateRect (hwnd, NULL, FALSE) ; DrumSetParams (&drum) ;bNeedSave = FALSE ; return 0 ; case IDM_FILE_OPEN:// Save previous file if (bNeedSave && IDCANCEL ==AskAboutSave (hwnd, szTitleName)) return 0 ; // Open the selected file if (DrumFileOpenDlg (hwnd, szFileName, szTitleName)) { szError = DrumFileRead (&drum, szFileName) ; if (szError != NULL) {ErrorMessage (hwnd, szError, szTitleName) ; szTitleName [0] = '\0' ; }else {// Set new parametersTempo = (int) (50 * (log10 (drum.iMsecPerBeat) - 1)) ; SetScrollPos (hwnd, SB_VERT, iTempo, TRUE) ;SetScrollPos (hwnd, SB_HORZ, drum.iVelocity, TRUE) ; DrumSetParams (&drum) ;InvalidateRect (hwnd, NULL, FALSE) ;bNeedSave = FALSE ; } DoCaption (hwnd, szTitleName) ; } return 0 ; case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: // Save the selected file if ((LOWORD (wParam) == IDM_FILE_SAVE && szTitleName [0]) || DrumFileSaveDlg (hwnd, szFileName, szTitleName)) { szError = DrumFileWrite (&drum, szFileName) ; if (szError != NULL) { ErrorMessage (hwnd, szError, szTitleName) ; szTitleName [0] = '\0' ; } else bNeedSave = FALSE ; DoCaption (hwnd, szTitleName) ; } return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L) ; return 0 ; case IDM_SEQUENCE_RUNNING: // Begin sequence if (!DrumBeginSequence (hwnd)) {ErrorMessage (hwnd,TEXT ("Could not start MIDI sequence -- ") TEXT ("MIDI Mapper device is unavailable!"),szTitleName) ; }else { CheckMenuItem (hMenu, IDM_SEQUENCE_RUNNING, MF_CHECKED) ; CheckMenuItem (hMenu, IDM_SEQUENCE_STOPPED, MF_UNCHECKED) ; } return 0 ; case IDM_SEQUENCE_STOPPED: // Finish at end of sequence DrumEndSequence (FALSE) ; return 0 ; case IDM_APP_ABOUT: DialogBox (hInst, TEXT ("AboutBox"), hwnd, AboutProc) ; return 0 ; } return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: hdc = GetDC (hwnd) ; // Convert mouse coordinates to grid coordinates x = LOWORD (lParam) / cxChar - 40 ; y = 2 *HIWORD (lParam) / cyChar - 2 ; // Set a new number of beats of sequence if (x > 0 && x <= 32 && y < 0) { SetTextColor (hdc, RGB (255, 255, 255)) ;TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":|"), 2);SetTextColor (hdc, RGB (0, 0, 0)) ; if (drum.iNumBeats % 4 == 0) TextOut ( hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT ("."), 1) ; drum.iNumBeats = x ;TextOut (hdc, (40 + drum.iNumBeats) * cxChar, 0, TEXT (":|"), 2); bNeedSave = TRUE ; } // Set or reset a percussion instrument beat if (x >= 0 && x < 32 && y >= 0 && y < NUM_PERC) { if (message == WM_LBUTTONDOWN) drum.dwSeqPerc[y] ^= (1 << x) ; else drum.dwSeqPian[y] ^= (1 << x) ; DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ; bNeedSave = TRUE ; } ReleaseDC (hwnd, hdc) ; DrumSetParams (&drum) ; return 0 ; case WM_HSCROLL: // Change the note velocity switch (LOWORD (wParam)) { case SB_LINEUP: drum.iVelocity -= 1 ; break ; case SB_LINEDOWN:drum.iVelocity += 1 ; break ; case SB_PAGEUP: drum.iVelocity -= 8 ; break ; case SB_PAGEDOWN:drum.iVelocity += 8 ; break ; case SB_THUMBPOSITION:drum.iVelocity = HIWORD (wParam) ; break ; default: return 0 ; } drum.iVelocity = max (1, min (drum.iVelocity, 127)) ; SetScrollPos (hwnd, SB_HORZ, drum.iVelocity, TRUE) ; DrumSetParams (&drum) ; bNeedSave = TRUE ; return 0 ; case WM_VSCROLL: // Change the tempo switch (LOWORD (wParam)) { case SB_LINEUP: iTempo -= 1 ; break ; case SB_LINEDOWN:iTempo += 1 ; break ; case SB_PAGEUP: iTempo -= 10 ; break ;case SB_PAGEDOWN: iTempo += 10 ; break ; case SB_THUMBPOSITION: iTempo = HIWORD (wParam) ; break ; default: return 0 ; } iTempo = max (0, min (iTempo, 100)) ; SetScrollPos (hwnd, SB_VERT, iTempo, TRUE) ; drum.iMsecPerBeat = (WORD) (10 * pow (100, iTempo / 100.0)) ; DrumSetParams (&drum) ; bNeedSave = TRUE ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetTextAlign (hdc, TA_UPDATECP) ; SetBkMode (hdc, TRANSPARENT) ; // Draw the text strings and horizontal lines for (i = 0 ; i < NUM_PERC ; i++) {MoveToEx (hdc, i & 1 ? 20 * cxChar : cxChar, (2 * i + 3) * cyChar / 4, NULL) ; TextOut (hdc, 0, 0, szPerc [i], lstrlen (szPerc [i])) ; GetCurrentPositionEx (hdc, &point) ; MoveToEx (hdc, point.x + cxChar, point.y + cyChar / 2, NULL) ; LineTo (hdc, 39 * cxChar, point.y + cyChar / 2) ; } SetTextAlign (hdc, 0) ; // Draw rectangular grid, repeat mark, and beat marks for (x = 0 ; x < 32 ; x++) { for (y = 0 ; y < NUM_PERC ; y++) DrawRectangle (hdc, x, y, drum.dwSeqPerc, drum.dwSeqPian) ; SetTextColor (hdc, x == drum.iNumBeats - 1 ? RGB (0, 0, 0) : RGB (255, 255, 255)) ; TextOut (hdc, (41 + x) * cxChar, 0, TEXT (":|"), 2) ; SetTextColor (hdc, RGB (0, 0, 0)) ; if (x % 4 == 0) TextOut (hdc, (40 + x) * cxChar, 0, TEXT ("."), 1) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_USER_NOTIFY: // Draw the "bouncing ball" hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (NULL_PEN)) ; SelectObject (hdc, GetStockObject (WHITE_BRUSH)) ; for (i = 0 ; i < 2 ; i++) { x = iIndexLast ; y = NUM_PERC + 1 ; Ellipse (hdc, (x + 40) * cxChar, (2 * y + 3) * cyChar / 4, (x + 41) * cxChar, (2 * y + 5) * cyChar / 4); iIndexLast = wParam ; SelectObject (hdc, GetStockObject (BLACK_BRUSH)) ; } ReleaseDC (hwnd, hdc) ; return 0 ; case WM_USER_ERROR: ErrorMessage (hwnd, TEXT ("Can't set timer event for tempo"), szTitleName) ; // fall through case WM_USER_FINISHED: DrumEndSequence (TRUE) ; CheckMenuItem (hMenu, IDM_SEQUENCE_RUNNING, MF_UNCHECKED) ; CheckMenuItem (hMenu, IDM_SEQUENCE_STOPPED, MF_CHECKED) ; return 0 ; case WM_CLOSE: if (!bNeedSave || IDCANCEL != AskAboutSave (hwnd, szTitleName)) DestroyWindow (hwnd) ; return 0 ; case WM_QUERYENDSESSION: if (!bNeedSave || IDCANCEL != AskAboutSave (hwnd, szTitleName)) return 1L ;return 0 ; case WM_DESTROY: DrumEndSequence (TRUE) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; } BOOL CALLBACK AboutProc ( HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) { case WM_INITDIALOG: return TRUE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDOK: EndDialog (hDlg, 0) ; return TRUE ; } break ; } return FALSE ; } void DrawRectangle (HDC hdc, int x, int y, DWORD * dwSeqPerc, DWORD * dwSeqPian) { int iBrush ; if (dwSeqPerc [y] & dwSeqPian [y] & (1L << x)) iBrush = BLACK_BRUSH ; else if (dwSeqPerc [y] & (1L << x)) iBrush = DKGRAY_BRUSH ; else if (dwSeqPian [y] & (1L << x)) iBrush = LTGRAY_BRUSH ; else iBrush = WHITE_BRUSH ; SelectObject (hdc, GetStockObject (iBrush)) ; Rectangle (hdc, (x + 40) * cxChar , (2 * y + 4) * cyChar / 4, (x + 41) * cxChar + 1, (2 * y + 6) * cyChar / 4 + 1) ; } void ErrorMessage (HWND hwnd, TCHAR * szError, TCHAR * szTitleName) { wsprintf (szBuffer, szError, (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szBuffer, szAppName, MB_OK | MB_ICONEXCLAMATION) ; } void DoCaption (HWND hwnd, TCHAR * szTitleName) { wsprintf (szBuffer, TEXT ("MIDI Drum Machine - %s"),(LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ; SetWindowText (hwnd, szBuffer) ; } int AskAboutSave (HWND hwnd, TCHAR * szTitleName) { int iReturn ; wsprintf (szBuffer, TEXT ("Save current changes in %s?"), (LPSTR) (szTitleName [0] ? szTitleName : szUntitled)) ; iReturn = MessageBox (hwnd, szBuffer, szAppName, MB_YESNOCANCEL | MB_ICONQUESTION) ; if (iReturn == IDYES) if (!SendMessage (hwnd, WM_COMMAND, IDM_FILE_SAVE, 0)) iReturn = IDCANCEL ; return iReturn ; }
DRUMTIME.H /*-------------------------------------------------------------------------- DRUMTIME.H Header File for Time Functions for DRUM Program ----------------------------------------------------------------------------*/ #define NUM_PERC 47 #define WM_USER_NOTIFY (WM_USER + 1) #define WM_USER_FINISHED (WM_USER + 2) #define WM_USER_ERROR (WM_USER + 3) #pragma pack(push, 2) typedef struct { short iMsecPerBeat ; short iVelocity ; short iNumBeats ; DWORD dwSeqPerc [NUM_PERC] ; DWORD dwSeqPian [NUM_PERC] ; } DRUM, * PDRUM ; #pragma pack(pop) void DrumSetParams(PDRUM) ; BOOL DrumBeginSequence (HWND) ; void DrumEndSequence (BOOL) ;
DRUMTIME.C /*----------------------------------------------------------------------------- DRUMFILE.C --Timer Routines for DRUM(c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include "drumtime.h" #define minmax(a,x,b) (min (max (x, a), b)) #define TIMER_RES 5 void CALLBACK DrumTimerFunc (UINT, UINT, DWORD, DWORD, DWORD) ; BOOL bSequenceGoing, bEndSequence ; DRUM drum ; HMIDIOUT hMidiOut ; HWND hwndNotify ; intiIndex ; UINT uTimerRes, uTimerID ; DWORD MidiOutMessage ( HMIDIOUT hMidi, int iStatus, int iChannel,int iData1, int iData2) { DWORD dwMessage ; dwMessage = iStatus | iChannel | (iData1 << 8) | (iData2 << 16) ; return midiOutShortMsg (hMidi, dwMessage) ; } void DrumSetParams (PDRUM pdrum) { CopyMemory (&drum, pdrum, sizeof (DRUM)) ; } BOOL DrumBeginSequence (HWND hwnd) { TIMECAPS tc ; hwndNotify = hwnd ; // Save window handle for notification DrumEndSequence (TRUE) ; // Stop current sequence if running // Open the MIDI Mapper output port if (midiOutOpen (&hMidiOut, MIDIMAPPER, 0, 0, 0)) return FALSE ; // Send Program Change messages for channels 9 and 0 MidiOutMessage (hMidiOut, 0xC0, 9, 0, 0) ; MidiOutMessage (hMidiOut, 0xC0, 0, 0, 0) ; // Begin sequence by setting a timer event timeGetDevCaps (&tc, sizeof (TIMECAPS)) ; uTimerRes = minmax (tc.wPeriodMin, TIMER_RES, tc.wPeriodMax) ; timeBeginPeriod (uTimerRes) ; uTimerID = timeSetEvent(max ((UINT) uTimerRes, (UINT) drum.iMsecPerBeat), uTimerRes, DrumTimerFunc, 0, TIME_ONESHOT) ; if (uTimerID == 0) { timeEndPeriod (uTimerRes) ; midiOutClose (hMidiOut) ; return FALSE ; } iIndex = -1 ; bEndSequence = FALSE ; bSequenceGoing = TRUE ; return TRUE ; } void DrumEndSequence (BOOL bRightAway) { if (bRightAway) { if (bSequenceGoing) { // stop the timer if (uTimerID) timeKillEvent (uTimerID) ; timeEndPeriod (uTimerRes) ; // turn off all notesMidiOutMessage (hMidiOut, 0xB0, 9, 123, 0) ; MidiOutMessage (hMidiOut, 0xB0, 0, 123, 0) ; // close the MIDI port midiOutClose (hMidiOut) ; bSequenceGoing = FALSE ; } } else bEndSequence = TRUE ; } void CALLBACK DrumTimerFunc ( UINT uID, UINT uMsg, DWORD dwUser, DWORD dw1, DWORD dw2) { static DWORD dwSeqPercLast [NUM_PERC], dwSeqPianLast [NUM_PERC] ; int i ; // Note Off messages for channels 9 and 0 if (iIndex != -1) { for (i = 0 ; i < NUM_PERC ; i++) { if (dwSeqPercLast[i] & 1 << iIndex) MidiOutMessage (hMidiOut, 0x80, 9, i + 35, 0) ; if (dwSeqPianLast[i] & 1 << iIndex)MidiOutMessage (hMidiOut, 0x80, 0, i + 35, 0) ; } } // Increment index and notify window to advance bouncing ball iIndex = (iIndex + 1) % drum.iNumBeats ; PostMessage (hwndNotify, WM_USER_NOTIFY, iIndex, timeGetTime ()) ; // Check if ending the sequence if (bEndSequence && iIndex == 0) { PostMessage (hwndNotify, WM_USER_FINISHED, 0, 0L) ;return ; } // Note On messages for channels 9 and 0 for (i = 0 ; i < NUM_PERC ; i++) { if (drum.dwSeqPerc[i] & 1 << iIndex) MidiOutMessage (hMidiOut, 0x90, 9, i + 35, drum.iVelocity) ; if (drum.dwSeqPian[i] & 1 << iIndex) MidiOutMessage (hMidiOut, 0x90, 0, i + 35, drum.iVelocity) ; dwSeqPercLast[i] = drum.dwSeqPerc[i] ; dwSeqPianLast[i] = drum.dwSeqPian[i] ; } // Set a new timer event uTimerID = timeSetEvent (max ((int) uTimerRes, drum.iMsecPerBeat), uTimerRes, DrumTimerFunc, 0, TIME_ONESHOT) ; if (uTimerID == 0) { PostMessage (hwndNotify, WM_USER_ERROR, 0, 0) ; } }
DRUMFILE.H /*--------------------------------------------------------------------------- DRUMFILE.H Header File for File I/O Routines for DRUM -----------------------------------------------------------------------------*/ BOOL DrumFileOpenDlg (HWND, TCHAR *, TCHAR *) ; BOOL DrumFileSaveDlg (HWND, TCHAR *, TCHAR *) ; TCHAR * DrumFileWrite (DRUM *, TCHAR *) ; TCHAR * DrumFileRead (DRUM *, TCHAR *) ;
DRUMFILE.C /*---------------------------------------------------------------------------- DRUMFILE.C -- File I/O Routines for DRUM(c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #include <commdlg.h> #include "drumtime.h" #include "drumfile.h" OPENFILENAME ofn = { sizeof (OPENFILENAME) } ; TCHAR * szFilter[] = { TEXT ("Drum Files (*.DRM)"), TEXT ("*.drm"), TEXT ("") } ; TCHAR szDrumID[]= TEXT ("DRUM") ; TCHAR szListID [] = TEXT ("LIST") ; TCHAR szInfoID [] = TEXT ("INFO") ; TCHAR szSoftID [] = TEXT ("ISFT") ; TCHAR szDateID [] = TEXT ("ISCD") ; TCHAR szFmtID [] = TEXT ("fmt ") ; TCHAR szDataID [] = TEXT ("data") ; char szSoftware []= "DRUM by Charles Petzold, Programming Windows" ; TCHAR szErrorNoCreate [] = TEXT ("File %s could not be opened for writing."); TCHAR szErrorCannotWrite [] = TEXT ("File %s could not be written to. ") ; TCHAR szErrorNotFound [] = TEXT ("File %s not found or cannot be opened.") ; TCHAR szErrorNotDrum [] = TEXT ("File %s is not a standard DRUM file.") ; TCHAR szErrorUnsupported [] = TEXT ("File %s is not a supported DRUM file.") ; TCHAR szErrorCannotRead[] = TEXT ("File %s cannot be read.") ; BOOL DrumFileOpenDlg (HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName) { ofn.hwndOwner= hwnd ; ofn.lpstrFilter= szFilter [0] ; ofn.lpstrFile= szFileName ; ofn.nMaxFile = MAX_PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle= MAX_PATH ; ofn.Flags = OFN_CREATEPROMPT ; ofn.lpstrDefExt = TEXT ("drm") ; return GetOpenFileName (&ofn) ; } BOOL DrumFileSaveDlg ( HWND hwnd, TCHAR * szFileName, TCHAR * szTitleName) { ofn.hwndOwner= hwnd ; ofn.lpstrFilter = szFilter [0] ; ofn.lpstrFile= szFileName ; ofn.nMaxFile = MAX_PATH ; ofn.lpstrFileTitle = szTitleName ; ofn.nMaxFileTitle= MAX_PATH ; ofn.Flags = OFN_OVERWRITEPROMPT ; ofn.lpstrDefExt = TEXT ("drm") ; return GetSaveFileName (&ofn) ; } TCHAR * DrumFileWrite (DRUM * pdrum, TCHAR * szFileName) { char szDateBuf [16] ; HMMIOhmmio ; int iFormat = 2 ; MMCKINFO mmckinfo [3] ; SYSTEMTIMEst ; WORD wError = 0 ; memset (mmckinfo, 0, 3 * sizeof (MMCKINFO)) ; // Recreate the file for writing if ((hmmio = mmioOpen (szFileName, NULL, MMIO_CREATE | MMIO_WRITE | MMIO_ALLOCBUF)) == NULL)return szErrorNoCreate ; // Create a "RIFF" chunk with a "CPDR" type mmckinfo[0].fccType = mmioStringToFOURCC (szDrumID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[0], MMIO_CREATERIFF) ; // Create "LIST" sub-chunk with an "INFO" type mmckinfo[1].fccType = mmioStringToFOURCC (szInfoID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[1], MMIO_CREATELIST) ; // Create "ISFT" sub-sub-chunk mmckinfo[2].ckid = mmioStringToFOURCC (szSoftID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[2], 0) ; wError |= (mmioWrite (hmmio, szSoftware, sizeof (szSoftware)) != sizeof (szSoftware)) ; wError |= mmioAscend (hmmio, &mmckinfo[2], 0) ; // Create a time string GetLocalTime (&st) ; wsprintfA (szDateBuf, "%04d-%02d-%02d", st.wYear, st.wMonth, st.wDay) ; // Create "ISCD" sub-sub-chunk mmckinfo[2].ckid = mmioStringToFOURCC (szDateID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[2], 0) ; wError |= (mmioWrite (hmmio, szDateBuf, (strlen (szDateBuf) + 1)) !=(int) (strlen (szDateBuf) + 1)) ; wError |= mmioAscend (hmmio, &mmckinfo[2], 0) ; wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ; // Create "fmt " sub-chunk mmckinfo[1].ckid = mmioStringToFOURCC (szFmtID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[1], 0) ; wError |= (mmioWrite (hmmio, (PSTR) &iFormat,sizeof (int)) != sizeof (int)) ; wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ; // Create the "data" sub-chunk mmckinfo[1].ckid = mmioStringToFOURCC (szDataID, 0) ; wError |= mmioCreateChunk (hmmio, &mmckinfo[1], 0) ; wError |= (mmioWrite (hmmio, (PSTR) pdrum, sizeof (DRUM)) != sizeof (DRUM)) ; wError |= mmioAscend (hmmio, &mmckinfo[1], 0) ; wError |= mmioAscend (hmmio, &mmckinfo[0], 0) ; // Clean up and return wError |= mmioClose (hmmio, 0) ; if (wError) { mmioOpen (szFileName, NULL, MMIO_DELETE) ; return szErrorCannotWrite ; } return NULL ; } TCHAR * DrumFileRead (DRUM * pdrum, TCHAR * szFileName) { DRUM drum ; HMMIO hmmio ; int i, iFormat ; MMCKINFO mmckinfo [3] ; ZeroMemory(mmckinfo, 2 * sizeof (MMCKINFO)) ; // Open the file if ((hmmio = mmioOpen (szFileName, NULL, MMIO_READ)) == NULL) return szErrorNotFound ; // Locate a "RIFF" chunk with a "DRUM" form-type mmckinfo[0].ckid = mmioStringToFOURCC (szDrumID, 0) ; if (mmioDescend (hmmio, &mmckinfo[0], NULL, MMIO_FINDRIFF)) { mmioClose (hmmio, 0) ; return szErrorNotDrum ; } // Locate, read, and verify the "fmt " sub-chunk mmckinfo[1].ckid = mmioStringToFOURCC (szFmtID, 0) ; if (mmioDescend (hmmio, &mmckinfo[1], &mmckinfo[0], MMIO_FINDCHUNK)) { mmioClose (hmmio, 0) ; return szErrorNotDrum ; } if (mmckinfo[1].cksize != sizeof (int)) { mmioClose (hmmio, 0) ; return szErrorUnsupported ; } if (mmioRead (hmmio, (PSTR) &iFormat, sizeof (int)) != sizeof (int)) { mmioClose (hmmio, 0) ; return szErrorCannotRead ; } if (iFormat != 1 && iFormat != 2) { mmioClose (hmmio, 0) ; return szErrorUnsupported ; } // Go to end of "fmt " sub-chunk mmioAscend (hmmio, &mmckinfo[1], 0) ; // Locate, read, and verify the "data" sub-chunk mmckinfo[1].ckid = mmioStringToFOURCC (szDataID, 0) ; if (mmioDescend (hmmio, &mmckinfo[1], &mmckinfo[0], MMIO_FINDCHUNK)) { mmioClose (hmmio, 0) ; return szErrorNotDrum ; } if (mmckinfo[1].cksize != sizeof (DRUM)) { mmioClose (hmmio, 0) ; return szErrorUnsupported ; } if (mmioRead (hmmio, (LPSTR) &drum, sizeof (DRUM)) != sizeof (DRUM)) { mmioClose (hmmio, 0) ; return szErrorCannotRead ; } // Close the file mmioClose (hmmio, 0) ; // Convert format 1 to format 2 and copy the DRUM structure data if (iFormat == 1) { for (i = 0 ; i < NUM_PERC ; i++) { drum.dwSeqPerc [i] = drum.dwSeqPian [i] ; drum.dwSeqPian [i] = 0 ; } } memcpy (pdrum, &drum, sizeof (DRUM)) ; return NULL ; }
DRUM.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu DRUMMENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New",IDM_FILE_NEW MENUITEM "&Open...",IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As...", IDM_FILE_SAVE_AS MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Sequence" BEGIN MENUITEM "&Running",IDM_SEQUENCE_RUNNING MENUITEM "&Stopped",IDM_SEQUENCE_STOPPED , CHECKED END POPUP "&Help" BEGIN MENUITEM "&About...", IDM_APP_ABOUT END END ///////////////////////////////////////////////////////////////////////////// // Icon DRUM ICON DISCARDABLE "drum.ico" ///////////////////////////////////////////////////////////////////////////// // Dialog ABOUTBOX DIALOG DISCARDABLE 20, 20, 160, 164 STYLE DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU CAPTION "Dialog" FONT 8, "MS Sans Serif" BEGIN DEFPUSHBUTTON "OK",IDOK,54,143,50,14 ICON"DRUM",IDC_STATIC,8,8,21,20 CTEXT "DRUM",IDC_STATIC,34,12,90,8 CTEXT "MIDI Drum Machine",IDC_STATIC,7,36,144,8 CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME,8,88,144,46 LTEXT "Left Button:\t\tDrum sounds",IDC_STATIC,12,92,136,8 LTEXT "Right Button:\t\tPiano sounds",IDC_STATIC,12,102,136,8 LTEXT "Horizontal Scroll:\t\tVelocity",IDC_STATIC,12,112,136,8 LTEXT "Vertical Scroll:\t\tTempo",IDC_STATIC,12,122,136,8 CTEXT "Copyright (c) Charles Petzold, 1998",IDC_STATIC,8,48, 144,8 CTEXT """Programming Windows,"" 5th Edition",IDC_STATIC,8,60, 144,8 END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by Drum.rc #defineIDM_FILE_NEW40001 #defineIDM_FILE_OPEN 40002 #defineIDM_FILE_SAVE 40003 #defineIDM_FILE_SAVE_AS40004 #defineIDM_APP_EXIT40005 #defineIDM_SEQUENCE_RUNNING40006 #defineIDM_SEQUENCE_STOPPED40007 #defineIDM_APP_ABOUT 40008
当第一次执行DRUM时,您将看到在窗口中有两列,左边一列按名称列出了47种不同的打击乐器。右边的网格是打击乐器的声音与时间的二维数组。每一个打击器都对应网格中的一列。32行就是32拍。如果要让这32拍出现在一个4/4拍的小节中(即每小节4个四分音符),那么每1拍对应一个三十二分音符。
从「Sequence」菜单选择「Running」时,程序将试图打开MIDIMapper设备。如果失败,屏幕将出现一个消息框。否则,您将看到一个「跳动的小球」随演奏的节拍在网格底部跳过。
在网格的任何位置单击鼠标左键可以在此拍中演奏打击乐器的声音,这时区域将变成暗灰色。用鼠标右键还可以添加钢琴的拍子,这时区域将会变成亮灰色。如果按下两个键(同时或分别),此区域将变成黑色,而且可以同时听到打击乐器和钢琴的声音。再次单击其中的一个键或双键将关闭该拍中的声音。
网格上部是每4拍一个点。这些点使我们不用过多的计算就可以很简易地确定单击的位置。网格的右上角是一个冒号和一条竖线(:|),它们看起来像传统音乐符号中的反复记号。这个符号表示序列的长度。您可以通过单击鼠标来将反复记号放置于网格内的任意位置。该序列最多(但不包括)只能演奏反复记号以内的拍子。如果要建立华尔兹节奏,则应将反复记号设定为3拍的若干倍。
水平滚动条控制MIDI NoteOn消息中的速率字节。这虽然能改变一些合成器的音质,但一般会影响音量。程序起初将速率滚动条设定在中间位置。竖直滚动条控制拍子。这是对数刻度,范围从每拍1秒(滚动条在底部)到每拍10毫秒(滚动条在顶部)。程序最初将拍子设定为每拍100毫秒(1/10秒),这时滚动条在中间。
「File」菜单允许您储存和读取扩展名为.DRM的文件,这是我定义的一种格式。这些文件很小并采用了RIFF的文件格式,这是一种所有新的多媒体数据文件推荐使用的格式。「Help」菜单中的「About」选项显示一个对话框,该对话框用一段非常简明的摘要来说明鼠标在网格中的用法以及两个滚动条的功能。
最后,「Sequence」菜单中的「Stopped」选项用于目前序列结束后终止乐曲并关闭MIDIMapper设备。
多媒体time函数
您可能会注意到DRUM.C没有呼叫任何多媒体函数。而所有的实际操作都发生在DRUMTIME模块中。
虽然普通的Windows定时器使用起来很简单,但它对实时时间应用却有灾难性的影响。就像我们在BACHTOCC程序中所看到的一样,演奏音乐就是这样的一种实时时间应用,对此Windows定时器是不合适的。为了提供在PC上演奏MIDI所需要的精确度,多媒体API还包括一个高分辨率的定时器,此定时器通过7个前缀是time的函数实作。这些函数有一个是多余的,而DRUMTIME展示了其余6个函数的用途。定时器函数将处理执行在一个单独执行绪中的callback函数。系统将按照程序指定的定时器延迟时间来呼叫定时器。
处理多媒体定时器时,可以用毫秒指定两种不同的时间。第一个是延迟时间,第二个称为分辨率。您可以认为分辨率是容错误差。如果指定一个延迟100毫秒,而分辨率是10毫秒,则定时器的实际延迟范围在90到110毫秒之间。
使用定时器之前,应获得定时器的设备能力:
timeGetDevCaps (&timecaps, uSize) ;
第一个参数是TIMECAPS型态结构的指针,第二个参数是此结构的大小。TIMECAPS结构只有两个字段,wPeriodMin和wPeriodMax。这是定时器设备驱动程序所支持的最小和最大的分辨率值。如果呼叫timeGetDevCaps后再查看这些值,会发现wPeriodMin是1而wPeriodMax是65535,所以此函数并不是很重要。不过,得到这些分辨率值并用于其它定时器函数呼叫是个好主意。
下一步呼叫
timeBeginPeriod (uResolution) ;
来指出程序所需要的定时器分辨率的最低值。该值应在TIMECAPS结构所确定的范围之内。此呼叫允许为可能使用定时器的多个程序提供最好的定时器设备驱动程序。呼叫timeBeginPeriod及timeEndPeriod必须成对出现,我将在后面对timeEndPeriod作简短的描述。
现在可以真正设定一个定时器事件:
idTimer = timeSetEvent ( uDelay, uResolution, CallBackFunc, dwData, uFlag) ;
如果发生错误,从呼叫传回的idTimer将是0。在呼叫的下面,将从Windows里用uDelay毫秒来呼叫CallBackFunc函数,其中允许的误差由uResolution指定。uResolution值必须大于或等于传递给timeBeginPeriod的分辨率。dwData是程序定义的数据,后来传递给CallBackFunc。最后一个参数可以是TIME_ONESHOT,也可以是TIME_PERIODIC。前者用于在uDelay毫秒数中获得一次CallBackFunc呼叫,而后者用于每个uDelay毫秒都获得一次CallBackFunc呼叫。
要在呼叫CallBackFunc之前终止只发生一次的定时器事件,或者暂停周期性的定时器事件,请呼叫
timeKillEvent (idTimer) ;
呼叫CallBackFunc后不必删除只发生一次的定时器事件。在程序中用完定时器以后,请呼叫
timeEndPeriod (wResolution) ;
其中的参数与传递给timeBeginPeriod的相同。
另两个函数的前缀是time。函数
dwSysTime = timeGetTime () ;
传回从Windows第一次启动到现在的系统时间,单位是毫秒。函数
timeGetSystemTime (&mmtime, uSize) ;
需要一个MMTIME结构的指针(与第一个参数一样),以及此结构的大小(与第二个参数一样)。虽然MMTIME结构可以在其它环境中用来得到非毫秒格式的系统时间,但此例中它都传回毫秒时间。所以timeGetSystemTime是多余的。
Callback函数只限于它所能做的Windows函数呼叫中。Callback函数可以呼叫PostMessage,PostMessage包含有四个定时器函数(timeSetEvent、timeKillEvent、timeGetTime和多余的timeGetSystemTime)、两个MIDI输出函数(midiOutShortMsg和midiOutLongMsg)以及调试函数OutputDebugStr。
很明显,设计多媒体定时器主要是用于MIDI序列而很少用于其它方面。当然,可以使用PostMessage来通知定时器事件的窗口消息处理程序,而且窗口消息处理程序可以做任何它想做的事,只是不能响应定时器callback自身的准确性。
Callback函数有五个参数,但只使用了其中两个参数:从timeSetEvent传回的定时器ID和最初作为参数传递给timeSetEvent的dwData值。
DRUM.C模块呼叫DRUMTIME.C中的DrumSetParams函数有很多次-建立DRUM窗口时、使用者在网格上单击或者移动滚动条时、从磁盘上加载.DRM文件时以及清除网格时。DrumSetParams的唯一的参数是指向DRUM型态结构的指针,此结构型态在DRUMTIME.H定义。该结构以毫秒为单位储存拍子时间、速度(通常对应于音量)、序列中的拍数以及用于储存网格(为打击乐器和钢琴声设定)的两套47个32字节的整数。这些32位整数中的每一位都对应序列的一拍。DRUM.C模块将在静态内存中维护一个DRUM型态的结构,并在呼叫DrumSetParams时向它传递一个指标。DrumSetParams只简单地复制此结构的内容。
要启动序列,DRUM呼叫DRUMTIME中的DrumBeginSequence函数。唯一的参数就是窗口句柄,其作用是通知。DrumBeginSequence打开MIDIMapper输出设备,如果成功,则发送ProgramChange消息来为MIDI信道0和9选择乐器声音(这些通道是基于0的,所以9实际指的是MIDI通道10,即打击乐器通道。另一个通道用于钢琴声)。DrumBeginSequence透过呼叫timeGetDevCaps和timeBeginPeriod来继续工作。在TIMER_RES定义的理想定时器分辨率通常是5毫秒,但我定义了一个称作minmax的宏来计算从timeGetDevCaps传回的限制范围以内的分辨率。
下一个呼叫是timeSetEvent,用于确定拍子时间,计算分辨率、callback函数DrumTimerFunc以及TIME_ONESHOT常数。DRUMTIME用的是只发生一次的定时器,而不是周期性定时器,所以速度可以随序列的执行而动态变化。timeSetEvent呼叫之后,定时器设备驱动程序将在延迟时间结束以后呼叫DrumTimerFunc。
DrumTimerFunccallback是DRUMTIME.C中的函数,在DRUMTIME.C中有许多重要的操作。变量iIndex储存序列中目前的拍子。Callback从为目前演奏的声音发送MIDINoteOff消息开始。iIndex的初始值-1以防止第一次启动序列时发生这种情况。
接下来,iIndex递增并将其值连同使用者定义的一个WM_USER_NOTIFY消息一起传递给DRUM中的窗口句柄。wParam消息参数设定为iIndex,以便在DRUM.C中,WndProc能够移动网格底部的「跳动的小球」。
DrumTimerFunc将下列事件作为结束:把NoteOn消息发送给信道0和9的合成器上,并储存网格值以便下一次可以关闭声音,然后透过呼叫timeSetEvent来设定新的只发生一次的定时器事件。
要停止序列,DRUM呼叫DrumEndSequence,其中唯一的参数可以设定为TRUE或FALSE。如果是TRUE,则DrumEndSequence按下面的程序立即结束序列:删除所有待决的定时器事件,呼叫timeEndPeriod,向两个MIDI信道发送「allnotesoff」消息,然后关闭MIDI输出埠。当使用者决定终止程序时,DRUM用TRUE参数呼叫DrumEndSequence。
然而,当使用者在DRUM里的「Sequence」菜单中选择「Stop」时,程序将用FALSE作为参数呼叫DrumEndSequence。这就允许序列在结束之前完成目前的循环。DrumEndSequence透过把bEndSequence整体变量设定为NULL来响应此呼叫。如果bEndSequence是TRUE,并且拍子的索引值设定为0,则DrumTimerFunc把使用者定义的WM_USER_FINISHED消息发送给WndProc。WndProc必须通过用TRUE作为参数呼叫DrumEndSequence来响应该消息,以便正确地结束定时器和MIDI埠的使用。
RIFF文件I/O
DRUM程序也可以储存和检索储存在DRUM结构中信息的文件。这些文件格式都是RIFF(ResourceInterchange FileFormat:资源交换文件格式),即一般建议使用的多媒体文件型态。当然,您可以用标准文件I/O函数来读写RIFF文件,但更简便的方法是使用前缀是mmio(对「多媒体输入/输出」)的函数。
检查.WAV格式时我们发现,RIFF是标记文件格式,这意味着文件中的数据由不同长度的数据块组成。每个数据块都用一个标记来识别。一个标记就是一个4字节的ASCII字符串。这与32位整数的标记名称相比要容易些。标记的后面是数据块长度及其数据。因为文件中的信息不是位于文件开头固定的偏移量而是用标记定义,所以标记文件格式是通用的。这样,可以透过添加附加标记来增强文件格式。在读文件时,程序可以很容易地找到所需要的数据并跳过不需要的或者不理解的标记。
Windows中的RIFF文件由独立的数据块组成。一个数据块可以分为数据块类型、数据块大小以及数据本身。数据块类型是4字符的ASCII码标记,标记中间不能有空格,但末尾可以有。数据块大小是一个4字节(32位)的值,用于显示数据块的大小。数据本身必须占用偶数个字节,必要时可以在结尾补0。这样,数据块的每个部分都是从文件开头就字组对齐好了的。数据块大小不包括数据块类型和数据块大小所需要的8字节,并且不反映添加的数据。
对于一些数据块类型,数据块大小与特定文件无关,是相同的。在数据块是包含信息的固定长度的结构时,就是这种情况。其它情况下,数据块大小根据特定文件变化。
有两个特殊型态的数据块分别称为RIFF数据块和LIST数据块。其中,数据以一个4字符ASCII形式型态开始,后面是一个或多个子数据块。LIST数据块与RIFF数据块类似,只是数据以4字符的ASCII列表型态开始。RIFF数据块用于所有的RIFF文件,而LIST数据块只在文件内部用来合并相关子数据块。
一个RIFF文件就是一个RIFF数据块。因此,RIFF文件以字符串「RIFF」和一个表示文件长度减去8字节的32位值开始。(实际上,如果需要补充数据则文件可能会长一个字节。)
多媒体API包括16个前缀是mmio的函数,这些函数是专门为RIFF文件设计的。DRUMFILE.C中已经用到其中几个函数来读写DRUM数据文件。
要用mmio函数打开文件,则第一步是呼叫mmioOpen。函数传回一个文件句柄。mmioCreateChunk函数在文件中建立一个数据块,这使用MMCKINFO定义的数据块名称和特征。mmioWrite函数写入数据块。写完数据块以后,呼叫mmioAscend。传递给mmioAscend的MMCKINFO结构必须与前面通过传递给mmioCreateChunk来建立数据块的MMCKINFO结构相同。通过从目前文件指针中减去结构的dwDataOffset字段来执行mmioAscend函数,此文件指标现在位于数据块的结尾,并且此值储存在数据的前面。如果数据块在长度上不是2字节的倍数,则mmioAscend函数也填补数据。
RIFF文件由巢状组织的数据块套迭组成。为使mmioAscend正常工作,必须维护多个MMCKINFO结构,每个结构与文件中的一个曾级相联系。DRUM数据文件共有三级。因此,在DRUMFILE.C中的DrumFileWrite函数中,我为三个MMCKINFO结构定义了一个数组,可以分别标记为mmckinfo[0]、mmckinfo[1]和mmckinfo[2]。在第一次mmioCreateChunk呼叫中,mmckinfo[0]结构与DRUM形式型态一起用于建立RIFF型态的块。其后是第二次mmioCreateChunk呼叫,它用mmckinfo[1]与INFO列表型态一起建立LIST型态的数据块。
第三次mmioCreateChunk呼叫用mmckinfo[2]建立一个ISFT型态的数据块,此数据块用于识别建立数据文件的软件。下面的mmioWrite呼叫用于写字符串szSoftware,呼叫mmioAscent可用mmckinfo[2]来填充此数据块的数据块大小字段。这是第一个完整的数据块。下一个数据块也在LIST数据块内。程序继续用另一个mmioCreateChunk来呼叫建立ISCD(creationdata:建立数据)数据块,并再次使用mmckinfo[2]。在mmioWrite呼叫来写入数据块以后,使用mmckinfo[2]呼叫mmioAscend来填充数据块大小。现在写到了此数据块的结尾,也是LIST块的结尾。所以,要填充LIST数据块的数据块大小字段,可再次呼叫mmioAscend,这次使用mmckinfo[1],它最初用于建立LIST数据块。
要建立「fmt」和「data」数据块,mmioCreateChunk使用mmckinfo[1];mmioWrite呼叫的后面也使用mmckinfo[1]的mmioAscend。在这一点上,除了RIFF数据块本身以外,所有的数据块大小都填好了。这需要多次使用mmckinfo[0]来呼叫mmioAscend。虽然有多次呼叫,但只呼叫mmioClose一次。
看起来好像mmioAscend呼叫改变了目前的文件指标,而且它的确填充了数据块大小,但在函数传回时,在数据块结束(或可能因补充数据而增加1字节)以后,文件指针恢复到以前的位置。从应用的观点来看,所有的文件写入都是按从头到尾的顺序。
mmioOpen呼叫成功后,除了磁盘空间耗尽之外,不会发生其它错误。使用变量wError从mmioCreateChunk、mmioWrite、mmioAscend和mmioClose呼叫累计错误代码,如果磁盘空间不足则每个呼叫都会失败。如果发生了错误,则mmioOpen以MMIO_DELETE常数为参数来删除文件,并传回错误信息。
读RIFF文件与建立RIFF文件类似,只不过是呼叫mmioRead而不是mmioWrite,呼叫mmioDescend而不是mmioCreateChunk。「下降」(descend)到一个数据块,是指找到数据块位置,并把文件指针移动到数据块大小之后(或者在RIFF或LIST数据块类型的形式型态或者列表型态的后面)。从数据块「上升」指的是把文件指标移动到数据块的结尾。mmioDescend和mmioAscend函数都不能把文件指标移到文件的前一个位置。
DRUM以前的版本在1992年的《PCMagazine》发表。那时,Windows支持两个不同等级的MIDI合成器(称为「基本的」和「扩展的」)。那个程序写的文件有格式标识符1。本章的DRUM程序将格式标识符设定为2。不过,它可以读取并转换早期的格式。这在DrumFileRead例程中完成。