22. 聲音與音樂

Post date: 2012/3/23 上午 05:43:32

22. 聲音與音樂

在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是符合工業標準的樂器數位化介面(Musical Instrument Digital Interface)。這類硬體播放音符以回應短的二進位命令訊息。MIDI硬體通常還可以通過電纜連結到如音樂鍵盤等的MIDI輸入設備上。通常,外部的MIDI合成器也能夠添加到音效卡上。

現在,大多數PC上的CD-ROM驅動器都具備播放普通音樂CD的能力。這就是平常所說的「CD聲音」。來自波形聲音設備、MIDI設備以及CD聲音設備的輸出,一般在使用者的控制下用「音量控制」程式混合在一起。

另外幾種普遍的多媒體「設備」不需要額外的硬體。Windows視訊設備(也稱作AVI視訊設備)播放副檔名為.AVI(audio-video interleave:聲音視頻插格)的電影或動畫檔案。「ActiveMovie控制項」可以播放其他型態的電影,包括QuickTime和MPEG。PC上的顯示卡需要特定的硬體來協助播放這些電影。

還有個別PC使用者使用某種Pioneer雷射影碟機或者Sony VISCA系列錄放影機。這些設備都有序列埠介面,因此可由PC軟體來控制。某些顯示卡具有一種稱為「視窗影像(video in a window)」的功能,此功能允許一個外部的視訊信號與其他應用程式一起出現在Windows的螢幕上。這也可認為是一種多媒體設備。

API概述

在Windows中,API支援的多媒體功能主要分成兩個集合。它們通常稱為「低階」和「高階」介面。

低階介面是一系列函式,這些函式以簡短的說明性字首開頭,而且在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions(與高階函式一起)中列出。

低階的波形聲音輸入輸出函式的字首是waveIn和waveOut。我們將在本章看到這些函式。另外,本章還討論用midiOut函式來控制MIDI輸出設備。這些API還包括midiIn和midiStream函式。

本章還使用字首為time的函式,這些函式允許設定一個高解析度的計時器常式,其計時器的時間間隔速率最低能夠到1毫秒。此程式主要用於播放MIDI音樂。其他幾組函式包括聲音壓縮、視訊壓縮以及動畫和視訊序列,可惜的是本章不包括這些函式。

您還會注意到多媒體函式列表中七個帶有字首mci的函式,它們允許存取媒體控制介面(MCI:Media Control Interface)。這是一個高階的開放介面,用於控制多媒體PC中所有的多媒體硬體。MCI包括所有多媒體硬體都共有的許多命令,因為多媒體的許多方面都以磁帶答錄機這類設備播放/記錄方式為模型。您為輸入或輸出而「打開」一台設備,進而可以「錄音」(對於輸入)或者「播放」(對於輸出),並且結束後可以「關閉」設備。

MCI本身分為兩種形式。一種形式下,可以向MCI發送訊息,這類似於Windows訊息。這些訊息包括位元編碼標記和C資料結構。另一種形式下,可以向MCI發送文字字串。這個程式主要用於描述命令語言,此語言具有靈活的字串處理函式,但支援呼叫Windows API的函式不多。字串命令版的MCI還有利於交互研究和學習MCI,我們馬上就舉一個例子。MCI中的設備名稱包括CD聲音(cdaudio)、波形音響(waveaudio)、MIDI編曲器(sequencer)、影碟機(videodisc)、vcr、overlay(視窗中的類比視頻)、dat(digital audio tape:數位式錄頻磁帶)以及數位視頻(digitalvideo)。MCI設備分為「簡單型」和「混合型」。簡單型設備(如CD聲音)不使用檔案。混合型設備(如波形音響)則使用檔案。使用波形音響時,這些檔案的副檔名是.WAV。

存取多媒體硬體的另一種方法包括DirectX API,它超出了本書的範圍。

另外兩個高階多媒體函式也值得一提:MessageBeep和PlaySound,它們在 第三章 有示範。MessageBeep播放「控制台」的「聲音」中指定的聲音。PlaySound可播放磁碟上、記憶體中或者作為資源載入的.WAV檔案。本章的後面還會用到PlaySound函式。

用TESTMCI研究MCI

在Windows多媒體的早期,軟體開發套件含有一個名為MCITEST的C程式,它允許程式寫作者交談式輸入MCI命令並學習這些命令的工作方式。這個程式,至少是C語言版,顯然已經消失了。因此,我又重新建立了它,即程式22-1所示的TESTMCI程式。雖然我不認為目前程式碼與舊的程式碼有什麼區別,但現在的使用者介面還是依據以前的MCITEST程式,並且沒有使用現在的程式碼。

程式22-1 TESTMCI TESTMCI.C /*--------------------------------------------------------------------------- TESTMCI.C -- MCI Command String Tester (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_TIMER 1 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("TestMci") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { if (-1 == DialogBox (hInstance, szAppName, NULL, DlgProc)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; } return 0 ; } BOOL CALLBACK DlgProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndEdit ; int iCharBeg, iCharEnd, iLineBeg, iLineEnd, iChar, iLine, iLength ; MCIERROR error ; RECT rect ; TCHAR szCommand [1024], szReturn [1024], szError [1024], szBuffer [32] ; switch (message) { case WM_INITDIALOG: // Center the window on screen GetWindowRect (hwnd, &rect) ; SetWindowPos (hwnd, NULL, (GetSystemMetrics (SM_CXSCREEN) - rect.right + rect.left) / 2, (GetSystemMetrics (SM_CYSCREEN) - rect.bottom + rect.top) / 2, 0, 0, SWP_NOZORDER | SWP_NOSIZE) ; hwndEdit = GetDlgItem (hwnd, IDC_MAIN_EDIT) ; SetFocus (hwndEdit) ; return FALSE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDOK: // Find the line numbers corresponding to the selection SendMessage (hwndEdit, EM_GETSEL, (WPARAM) &iCharBeg, (LPARAM) &iCharEnd) ; iLineBeg = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharBeg, 0) ; iLineEnd = SendMessage (hwndEdit, EM_LINEFROMCHAR, iCharEnd, 0) ; // Loop through all the lines for (iLine = iLineBeg ; iLine <= iLineEnd ; iLine++) { // Get the line and terminate it; ignore if blank * (WORD *) szCommand = sizeof (szCommand) / sizeof (TCHAR) ; iLength = SendMessage (hwndEdit, EM_GETLINE, iLine, (LPARAM) szCommand) ; szCommand [iLength] = '\0' ; if (iLength == 0) continue ; // Send the MCI command error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ; // Set the Return String field SetDlgItemText (hwnd, IDC_RETURN_STRING, szReturn) ; // Set the Error String field (even if no error) mciGetErrorString (error, szError, sizeof (szError) / sizeof (TCHAR)) ; SetDlgItemText (hwnd, IDC_ERROR_STRING, szError) ; } // Send the caret to the end of the last selected line iChar = SendMessage (hwndEdit, EM_LINEINDEX, iLineEnd, 0) ; iChar += SendMessage (hwndEdit, EM_LINELENGTH, iCharEnd, 0) ; SendMessage (hwndEdit, EM_SETSEL, iChar, iChar) ; // Insert a carriage return/line feed combination SendMessage (hwndEdit, EM_REPLACESEL, FALSE, (LPARAM) TEXT ("\r\n")) ; SetFocus (hwndEdit) ; return TRUE ; case IDCANCEL: EndDialog (hwnd, 0) ; return TRUE ; case IDC_MAIN_EDIT: if (HIWORD (wParam) == EN_ERRSPACE) { MessageBox (hwnd, TEXT ("Error control out of space."), szAppName, MB_OK | MB_ICONINFORMATION) ; return TRUE ; } break ; } break ; case MM_MCINOTIFY: EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), TRUE) ; wsprintf (szBuffer, TEXT ("Device ID = %i"), lParam) ; SetDlgItemText (hwnd, IDC_NOTIFY_ID, szBuffer) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), wParam & MCI_NOTIFY_SUCCESSFUL) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), wParam & MCI_NOTIFY_SUPERSEDED) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), wParam & MCI_NOTIFY_ABORTED) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), wParam & MCI_NOTIFY_FAILURE) ; SetTimer (hwnd, ID_TIMER, 5000, NULL) ; return TRUE ; case WM_TIMER: KillTimer (hwnd, ID_TIMER) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_MESSAGE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ID), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUCCESSFUL), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_SUPERSEDED), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_ABORTED), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_NOTIFY_FAILURE), FALSE) ; return TRUE ; case WM_SYSCOMMAND: switch (LOWORD (wParam)) { case SC_CLOSE: EndDialog (hwnd, 0) ; return TRUE ; } break ; } return FALSE ; }

TESTMCI.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog TESTMCI DIALOG DISCARDABLE 0, 0, 270, 276 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "MCI Tester" FONT 8, "MS Sans Serif" BEGIN EDITTEXT IDC_MAIN_EDIT,8,8,254,100,ES_MULTILINE | ES_AUTOHSCROLL | WS_VSCROLL LTEXT "Return String:",IDC_STATIC,8,114,60,8 EDITTEXT IDC_RETURN_STRING,8,126,120,50,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_GROUP | NOT WS_TABSTOP LTEXT "Error String:",IDC_STATIC,142,114,60,8 EDITTEXT IDC_ERROR_STRING,142,126,120,50,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | NOT WS_TABSTOP GROUPBOX "MM_MCINOTIFY Message",IDC_STATIC,9,186,254,58 LTEXT "",IDC_NOTIFY_ID,26,198,100,8 LTEXT "MCI_NOTIFY_SUCCESSFUL",IDC_NOTIFY_SUCCESSFUL,26,212,100, 8,WS_DISABLED LTEXT "MCI_NOTIFY_SUPERSEDED",IDC_NOTIFY_SUPERSEDED,26,226,100, 8,WS_DISABLED LTEXT "MCI_NOTIFY_ABORTED",IDC_NOTIFY_ABORTED,144,212,100,8, WS_DISABLED LTEXT "MCI_NOTIFY_FAILURE",IDC_NOTIFY_FAILURE,144,226,100,8, WS_DISABLED DEFPUSHBUTTON "OK",IDOK,57,255,50,14 PUSHBUTTON "Close",IDCANCEL,162,255,50,14 END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by TestMci.rc #define IDC_MAIN_EDIT 1000 #define IDC_NOTIFY_MESSAGE 1005 #define IDC_NOTIFY_ID 1006 #define IDC_NOTIFY_SUCCESSFUL 1007 #define IDC_NOTIFY_SUPERSEDED 1008 #define IDC_NOTIFY_ABORTED 1009 #define IDC_NOTIFY_FAILURE 1010 #define IDC_SIGNAL_MESSAGE 1011 #define IDC_SIGNAL_ID 1012 #define IDC_SIGNAL_PARAM 1013 #define IDC_RETURN_STRING 1014 #define IDC_ERROR_STRING 1015 #define IDC_DEVICES 1016 #define IDC_STATIC -1

與本章的大多數程式一樣,TESTMCI使用非模態對話方塊作為它的主視窗。與本章所有的程式一樣,TESTMCI要求WINMM.LIB引用程式庫在Microsoft Visual C++「Projects Settings」對話方塊的「Links」頁列出。

此程式用到了兩個最重要的多媒體函式:mciSendString和mciGetErrorText。在TESTMCI的主編輯視窗輸入一些內容然後按下Enter鍵(或「OK」按鈕)後,程式將輸入的字串作為第一個參數傳遞給mciSendString命令:

如果在編輯視窗選擇了不止一行,則程式將按順序將它們發送給mciSendString函式。第二個參數是字串位址,此字串取得從函式傳回的資訊。程式將此資訊顯示在視窗的「Return String」區域。從mciSendString傳回的錯誤代碼傳遞給mciGetErrorString函式,以獲得文字錯誤說明;此說明顯示在TESTMCI視窗的「Error String」區域。

MCITEXT和CD聲音

通過控制CD-ROM驅動器和播放聲音CD,您會對MCI命令字串留下很好的印象。因為這些命令字串一般都非常簡單,並且更重要的是您可以聽到一些音樂,所以這是好的起點。您可以在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Command Strings中獲得MCI命令字串的參考,以方便本練習。

請確認CD-ROM驅動器的聲音輸出已連結到擴音器或耳機,然後放入一張聲音CD,如Bruce Springsteen的「Born to Run」。Windows 98中,「CD播放程式」將啟動並開始播放此唱片。如果是這樣的話,終止「CD播放程式」,然後可以叫出TESTMCI並且鍵入命令:

然後按Enter鍵。其中open是MCI命令,cdaudio是MCI認定的CD-ROM驅動器的設備名稱(假定您的系統中只有一個CD-ROM驅動器。要獲得多個CD-ROM驅動器名稱需使用sysinfo命令)。

TESTMCI中的「Return String」區域顯示mciSendString函式中系統傳回給程式的字串。如果執行了open命令,則此值是1。TESTMCI在「Error String」區域中顯示mciGetErrorString依據mciSendString傳回值所傳回的資訊。如果mciSendString沒有傳回錯誤代碼,則「Error String」區域顯示文字"The specified command was carried out"。

假定執行了open命令,現在就可以輸入:

CD將開始播放唱片上的第一首樂曲「Thunder Road」。輸入下面的命令可以暫停播放:

或者

對於CD聲音設備來說,這些敘述的功能相同。您可用下面的敘述重新播放:

迄今為止,我們使用的全部字串都由命令和設備名稱組成。其中有些命令帶有選項。例如,鍵入:

根據收聽時間的長短,「Return String」區域將顯示類似下面的一些字元:

這是些什麼?很顯然不是小時、分鐘和秒,因為CD沒有那麼長。要找出時間格式,請鍵入:

現在「Return String」區域顯示下面的字串:

這代表「分-秒-格」。CD聲音中,每秒有75格。時間格式的訊格部分可在0到74之間的範圍內變化。

狀態命令有一連串的選項。使用下面的命令,您可以確定msf格式的CD全部長度:

對於「Born to Run」,「Return String」區域將顯示:

這指的是39分28秒19格。

現在試一下

「Return String」區域將顯示:

我們從CD封面上知道「Born to Run」CD上第五首樂曲是主題曲。MCI命令中的樂曲從1開始編號。要想知道樂曲「Born to Run」的長度,可以鍵入下面的命令:

「Return String」區域將顯示:

我們還可確定此樂曲從盤上的哪個位置開始:

「Return String」區域將顯示:

根據這條資訊,我們可以直接跳到樂曲標題:

此命令只播放一首樂曲,然後停止。最後的值是由4:30:22(樂曲長度)加17:36:35得到的。或者,也可以用下面的命令確定:

或者,也可以將時間格式設定為樂曲-分-秒-格:

然後

或者,更簡單地

如果時間的尾部是0,那麼您可去掉它們。還可以用毫秒設定時間格式。

每個MCI命令字串都可以在字串的後面包括選項wait和notify(但不是同時使用)。例如,假設您只想播放「Born to Run」的前10秒,而且播放後,您還想讓程式完成其他工作。您可按下面的方法進行(假定您已經將時間格式設定為tmsf):

這種情況下,直到函式執行結束,也就是說,直到播放完「Born to Run」的前10秒,mciSendString函式才傳回。

現在很明顯,一般來說,在單執行緒的應用程式中這不是一件好事。如果不小心鍵入:

直到整個唱片播放完以後,mciSendString函式才將控制權傳回給程式。如果必須使用wait選項(在只要執行MCI描述檔案而不管其他事情的時候,這麼做很方便,與我將展示的一樣),首先使用break命令。此命令可設定一個虛擬鍵碼,此碼將中斷mciSendString命令並將控制權傳回給程式。例如,要設定Escape鍵來實作此目的,可用:

這裏,27是十進位的VK_ESCAPE值。

比wait選項更好的是notify選項:

這種情況下,mciSendString函式立即傳回,但如果該操作在MCI命令的尾部定義,則mciSendString函式的最後一個參數所指定代號的視窗會收到MM_MCINOTIFY訊息。TESTMCI程式在MM_MCINOTIFY框中顯示此訊息的結果。為避免與其他可能鍵入的命令混淆,TESTMCI程式在5秒後停止顯示MM_MCINOTIFY訊息的結果。

您可以同時使用wait和notify關鍵字,但沒有理由這麼做。不使用這兩個關鍵字,內定的操作就既不是wait,也不是您通常所希望的notify。

用這些命令結束播放時,可鍵入下面的命令來停止CD:

如果在關閉之前沒有停止CD-ROM設備,那麼甚至在關閉設備之後還會繼續播放CD。

另外,您還可以試試您的硬體允許或者不允許的一些命令:

最後按下面的方法關閉設備:

雖然TESTMCI自己不能儲存或載入文字檔案,但可以在編輯控制項和剪貼簿之間複製文字:先從TESTMCI選擇一些內容,將其複製到剪貼簿(用Ctrl-C),再將這些文字從剪貼簿複製到「記事本」,然後儲存。相反的操作,可以將一系列的MCI命令載入到TESTMCI。如果選擇了一系列命令然後按下「OK」按鈕(或者Enter鍵),則TESTMCI將每次執行一條命令。這就允許您編寫MCI的「描述檔案」,即MCI命令的簡單列表。

例如,假設您想聽歌曲「Jungleland」(唱片中的最後一首)、「Thunder Road」和「Born to Run」,並要按此順序聽,可以編寫如下的描述命令:

error = mciSendString (szCommand, szReturn, sizeof (szReturn) / sizeof (TCHAR), hwnd) ;

open cdaudio

play cdaudio

pause cdaudio

stop cdaudio

play cdaudio

status cdaudio position

01:15:25

status cdaudio time format

msf

status cdaudio length

39:28:19

status cdaudio number of tracks

8

status cdaudio length track 5

04:30:22

status cdaudio position track 5

17:36:35

play cdaudio from 17:36:35 to 22:06:57

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

play cdaudio from 5:0:0 to 5:0:10 wait

play cdaudio wait

break cdaudio on 27

play cdaudio from 5:0:0 to 5:0:10 notify

stop cdaudio

eject cdaudio

close cdaudio

open cdaudio set cdaudio time format tmsf break cdaudio on 27 play cdaudio from 8 wait play cdaudio from 1 to 2 wait play cdaudio from 5 to 6 wait stop cdaudio eject cdaudio close cdaudio

不用wait關鍵字,就不能正常工作,因為mciSendString命令會立即傳回,然後執行下一條命令。

此時,如何編寫模擬CD播放程式的簡單應用程式,就應該相當清楚了。程式可以確定樂曲數量、每個樂曲的長度並能顯示允許使用者從任意位置開始播放(不過,請記住:mciSendString總是傳回文字字串資訊,因此您需要編寫解析處理程式來將這些字串轉換成數字)。可以肯定,這樣的程式還要使用Windows計時器,以產生大約1秒的時間間隔。在WM_TIMER訊息處理期間,程式將呼叫:

來查看CD是暫停還是在播放。

命令允許程式更新顯示以給使用者顯示目前的位置。但可能還存在更令人感興趣的事:如果程式知道音樂音調部分的節拍位置,那麼就可以使螢幕上的圖形與CD同步。這對於音樂指令或者建立自己的圖形音樂視訊程式極為有用。

波形聲音

波形聲音是最常用的Windows多媒體特性。波形聲音設備可以通過麥克風捕捉聲音,並將其轉換為數值,然後把它們儲存到記憶體或者磁碟上的波形檔案中,波形檔案的副檔名是.WAV。這樣,聲音就可以播放了。

聲音與波形

在接觸波形聲音API之前,具備一些預備知識很重要,這些知識包括物理學、聽覺以及聲音進出電腦的程序。

聲音就是振動。當聲音改變了鼓膜上空氣的壓力時,我們就感覺到了聲音。麥克風可以感應這些振動,並且將它們轉換為電流。同樣,電流再經過放大器和擴音器,就又變成了聲音。傳統上,聲音以類比方式儲存(例如錄音磁帶和唱片),這些振動儲存在磁氣脈衝或者輪廓凹槽中。當聲音轉換為電流時,就可以用隨時間振動的波形來表示。振動最自然的形式可以用正弦波表示,它的一個週期如 圖5-5 所示。

正弦波有兩個參數-振幅(也就是一個週期中的最大振幅)和頻率。我們已知振幅就是音量,頻率就是音調。一般來說人耳可感受的正弦波的範圍是從20Hz(每秒週期)的低頻聲音到20,000Hz的高頻聲,但隨著年齡的增長,對高頻聲音的感受能力會逐年退化。

人感受頻率的能力與頻率是對數關係而不是線性關係。也就是說,我們感受20Hz到40Hz的頻率變化與感受40Hz到80Hz的頻率變化是一樣的。在音樂中,這種加倍的頻率定義為八度音階。因此,人耳可感覺到大約10個八度音階的聲音。鋼琴的範圍是從27.5 Hz到4186 Hz之間,略小於7個八度音階。

雖然正弦波代表了振動的大多數自然形式,但純正弦波很少在現實生活中單獨出現,而且,純正弦波並不動聽。大多數聲音都很複雜。

任何週期的波形(即,一個迴圈波形)可以分解成多個正弦波,這些正弦波的頻率都是整倍數。這就是所謂的Fourier級數,它以法國數學家和物理學家Jean Baptiste Joseph Fourier(1768-1830)的名字命名。週期的頻率是基礎。級數中其他正弦波的頻率是基礎頻率的2倍、3倍、4倍(等等)。這些頻率的聲音稱為泛音。基礎頻率也稱作一級諧波。第一泛音是二級諧波,以此類推。

正弦波諧波的相對強度給每個週期的波形唯一的聲音。這就是「音質」,它使得喇叭吹出喇叭聲,鋼琴彈出鋼琴聲。

人們一度認為電子合成樂器僅僅需要將聲音分解成諧波並且與多個正弦波重組即可。不過,事實證明現實世界中的聲音並不是這麼簡單。代表現實世界中聲音的波形都沒有嚴格的週期。樂器之間諧波的相對強度是不同的,並且諧波也隨著每個音符的演奏時間改變。特別是樂器演奏音符的開始位置-我們稱作起奏(attack)-相當複雜,但這個位置又對我們感受音質至關重要。

由於近年來數位儲存能力的提高,我們可以將聲音直接以數位形式儲存而不用複雜的重組。

脈衝編碼調製(Pulse Code Modulation)

電腦處理的是數值,因此要使聲音進入電腦,就必須設計一種能將聲音與數位信號相互轉換的機制。

不壓縮資料就完成此功能的最常用方法稱作「脈衝編碼調製」(PCM:pulse code modulation)。PCM可用在光碟、數位式錄音磁帶以及Windows中。脈衝編碼調製其實只是一種概念上很簡單的處理步驟的奇怪代名詞而已。

利用脈衝編碼調製,波形可以按固定的週期頻率取樣,其頻率通常是每秒幾萬次。對於每個樣本都測量其波形的振幅。完成將振幅轉換成數位信號工作的硬體是類比數位轉換器(ADC:analog-to-digital converter)。類似地,通過數位類比轉換器(DAC:digital-to-analog converter)可將數位信號轉換回波形電子信號。但這樣轉換得到的波形與輸入的並不完全相同。合成的波形具有由高頻組成的尖銳邊緣。因此,播放硬體通常在數位類比轉換器後還包括一個低通濾波器。此濾波器濾掉高頻,並使合成後的波形更平滑。在輸入端,低通濾波器位於ADC前面。

脈衝編碼調製有兩個參數:取樣頻率,即每秒內測量波形振幅的次數;樣本大小,即用於儲存振幅級的位元數。與您想像的一樣:取樣頻率越高,樣本大小越大,原始聲音的複製品才更好。不過,存在一個提高取樣頻率和樣本大小的極點,超過這個極點也就超過了人類分辨聲音的極限。另外,如果取樣頻率和樣本大小過低,將導致不能精確地複製音樂以及其他聲音。

取樣頻率

取樣頻率決定聲音可被數位化和儲存的最大頻率。尤其是,取樣頻率必須是樣本聲音最高頻率的兩倍。這就是「Nyquist頻率(Nyquist Frequency)」,以30年代研究取樣程序的工程師Harry Nyquist的名字命名。

以過低的取樣頻率對正弦波取樣時,合成的波形比最初的波形頻率更低。這就是所說的失真信號。為避免失真信號的發生,在輸入端使用低通濾波器以阻止頻率大於半個取樣頻率的所有波形。在輸出端,數位類比轉換器產生的粗糙的波形邊緣實際上是由頻率大於半個取樣頻率的波形組成的泛音。因此,位於輸出端的低通濾波器也阻止頻率大於半個取樣頻率的所有波形。

聲音CD中使用的取樣頻率是每秒44,100個樣本,或者稱為44.1kHz。這個特有的數值是這樣產生的:

人耳可聽到最高20kHz的聲音,因此要攔截人能聽到的整個聲音範圍,就需要40kHz的取樣頻率。然而,由於低通濾波器具有頻率下滑效應,所以取樣頻率應該再高出大約百分之十才行。現在,取樣頻率就達到了44kHz。這時,我們要與視訊同時記錄數位聲音,於是取樣頻率就應該是美國、歐洲電視顯示格速率的整數倍,這兩種視訊格速率分別是30Hz和25Hz。這就使取樣頻率升高到了44.1kHz。

取樣頻率為44.1kHz的光碟會產生大量的資料,這對於一些應用程式來說實在是太多了,例如對於錄製聲音而不是錄製音樂時就是這樣。把取樣頻率減半到22.05 kHz,可由一個10 kHz的泛音來簡化複製聲音的上半部分。再將其減半到11.025 kHz就向我們提供了5 kHz頻率範圍。44.1 kHz、22.05 kHz和11.025 kHz的取樣頻率,以及8 kHz都是波形聲音設備普遍支援的標準。

因為鋼琴的最高頻率為4186 Hz,所以您可能會認為給鋼琴錄音時,11.025 kHz的取樣頻率就足夠了。但4186 Hz只是鋼琴最高的基礎頻率而已,濾掉大於5000Hz的所有正弦波將減少可被複製的泛音,而這樣將不能精確地捕捉和複製鋼琴的聲音。

樣本大小

脈衝編碼調製的第二個參數是按位元計算的樣本大小。樣本大小決定了可供錄製和播放的最低音與最高音之間的區別。這就是通常所說的動態範圍。

聲音強度是波形振幅的平方(即每個正弦波一個週期中最大振幅的合成)。與頻率一樣,人對聲音強度的感受也呈對數變化。

兩個聲音在強度上的區別是以貝爾(以電話發明人Alexander Graham Bell的名字命名)和分貝(dB)為單位進行測量的。1貝爾在聲音強度上呈10倍增加。1dB就是以相同的乘法步驟成為1貝爾的十分之一。由此,1dB可增加聲音強度的1.26倍(10的10次方根),或者增加波形振幅的1.12倍(10的20次方根)。1分貝是耳朵可感覺出的聲強的最小變化。從開始能聽到的聲音極限到讓人感到疼痛的聲音極限之間的聲強差大約是100 dB。

可用下面的公式來計算兩個聲音間的動態範圍,單位是分貝:

其中A1和A2是兩個聲音的振幅。因為只可能有一個振幅,所以樣本大小是1位元,動態範圍是0。

如果樣本大小是8位元,則最大振幅與最小振幅之間的比例就是256。這樣,動態範圍就是:

或者48分貝。48的動態範圍大約相當於非常安靜的房屋與電動割草機之間的差別。將樣本大小加倍到16位元產生的動態範圍是:

或者96分貝。這非常接近聽覺極限和疼痛極限,而且人們認為這就是複製音樂的理想值。

Windows同時支援8位元和16位元的樣本大小。儲存8位元的樣本時,樣本以無正負號位元組處理,靜音將儲存為一個值為0x80的字串。16位元的樣本以帶正負號整數處理,這時靜音將儲存為一個值為0的字串。

要計算未壓縮聲音所需的儲存空間,可用以秒為單位的聲音持續時間乘以取樣頻率。如果用16位元樣本而不是8位元樣本,則將其加倍,如果是錄製立體聲則再加倍。例如,1小時的CD聲音(或者是在每個立體聲樣本占2位元組、每秒44 ,100個樣本的速度下進行3 600秒)需要635MB,這快要接近一張CD-ROM的儲存量了。

在軟體中產生正弦波

對於第一個關於波形聲音的練習,我們不打算將聲音儲存到檔案中或播放錄製的聲音。我們將使用低階的波形聲音API(即,字首是waveOut的函式)來建立一個稱作SINEWAVE的聲音正弦波生成器。此程式以1 Hz的增量來生成從20Hz(人可感覺的最低值)到5,000Hz(與人感覺的最高值相差兩個八度音階)的正弦波。

我們知道,標準C執行時期程式庫包括了一個sin函式,該函式傳回一個弧度角的正弦值(2π弧度等於360度)。sin函式傳回值的範圍是從-1到1(早在第五章,我們就在SINEWAVE程式中使用過這個函式)。因此,應該很容易使用sin函式生成輸出到波形聲音硬體的正弦波資料。基本上是用代表波形(這時是正弦波)的資料來填充緩衝區,並將此緩衝區傳遞給API。(這比前面所講的稍微有些複雜,但我將詳細介紹)。波形聲音硬體播放完緩衝區中的資料後,應將第二個緩衝區中的資料傳遞給它,並且以此類推。

第一次考慮這個問題(而且對PCM也一無所知)時,您大概會認為將一個週期的正弦波分成若干固定數量的樣本-例如360個-才合理。對於20 Hz的正弦波,每秒輸出7,200個樣本。對於200 Hz的正弦波,每秒則要輸出72,000個樣本。這有可能實作,但實際上卻不能這麼做。對於5,000 Hz的正弦波,就需要每秒輸出1,800,000個樣本,這的確會增大DAC的負擔!更重要的是,對於更高的頻率,這種作法會比實際需要的精確度還高。

就脈衝編碼調製而言,取樣頻率是個常數。假定取樣頻率是SINEWAVE程式中使用的11,025Hz。如果要生成一個2,756.25Hz(確切地說是四分之一的取樣頻率)的正弦波,則正弦波的每個週期就有4個樣本。對於25Hz的正弦波,每個週期就有441個樣本。通常,每週期的樣本數等於取樣頻率除以要得到的正弦波頻率。一旦知道了每週期的樣本數,用2π弧度除以此數,然後用sin函式來獲得每週期的樣本。然後再反覆對一個週期進行取樣,從而建立一個連續的波形。

問題是每週期的樣本數可能帶有小數,因此在使用時這種方法並不是很好。每個週期的尾部都會有間斷。

使它正常工作的關鍵是保留一個靜態的「相位角」變數。此角初始化為0。第一個樣本是0度正弦。隨後,相位角增加一個值,該值等於2π乘以頻率再除以取樣頻率。用此相位角作為第二個樣本,並且按此方法繼續。一旦相位角超過2π弧度,則減去2π弧度,而不要把相位角再初始化為0。

例如,假定要用11,025Hz的取樣頻率來生成1,000Hz的正弦波。即每週期有大約11個樣本。為便於理解,此處相位角按度數給出-大約前一個半週期的相位角是:0、32.65、65.31、97.96、130.61、163.27、195.92、228.57、261.22、293.88、326.53、359.18、31.84、64.49、97.14、129.80、162.45、195.10,以此類推。存入緩衝區的波形資料是這些角度的正弦值,並已縮放到每樣本的位元數。為後來的緩衝區建立資料時,可繼續增加最後的相位角,而不要將它初始化為0。

如程式22-2所示,FillBuffer函式完成這項工作-與SINEWAVE程式的其餘部分一起完成。

status cdaudio mode

status cdaudio position

程式22-2 SINEWAVE SINEWAVE.C /*------------------------------------------------------------------------- SINEWAVE.C -- Multimedia Windows Sine Wave Generator (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include "resource.h" #pragma comment(lib,"winmm.lib") #define SAMPLE_RATE 11025 #define FREQ_MIN 20 #define FREQ_MAX 5000 #define FREQ_INIT 440 #define OUT_BUFFER_SIZE 4096 #define PI 3.14159 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("SineWave") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { if (-1 == DialogBox (hInstance, szAppName, NULL, DlgProc)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; } return 0 ; } VOID FillBuffer (PBYTE pBuffer, int iFreq) { static double fAngle ; int i ; for (i = 0 ; i < OUT_BUFFER_SIZE ; i++) { pBuffer [i] = (BYTE) (127 + 127 * sin (fAngle)) ; fAngle += 2 * PI * iFreq / SAMPLE_RATE ; if ( fAngle > 2 * PI) fAngle -= 2 * PI ; } } BOOL CALLBACK DlgProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL bShutOff, bClosing ; static HWAVEOUT hWaveOut ; static HWND hwndScroll ; static int iFreq = FREQ_INIT ; static PBYTE pBuffer1, pBuffer2 ; static PWAVEHDR pWaveHdr1, pWaveHdr2 ; static WAVEFORMATEX waveformat ; int iDummy ; switch (message) { case WM_INITDIALOG: hwndScroll = GetDlgItem (hwnd, IDC_SCROLL) ; SetScrollRange (hwndScroll, SB_CTL, FREQ_MIN, FREQ_MAX, FALSE) ; SetScrollPos (hwndScroll, SB_CTL, FREQ_INIT, TRUE) ; SetDlgItemInt (hwnd, IDC_TEXT, FREQ_INIT, FALSE) ; return TRUE ; case WM_HSCROLL: switch (LOWORD (wParam)) { case SB_LINELEFT: iFreq -= 1 ; break ; case SB_LINERIGHT: iFreq += 1 ; break ; case SB_PAGELEFT: iFreq /= 2 ; break ; case SB_PAGERIGHT: iFreq *= 2 ; break ; case SB_THUMBTRACK: iFreq = HIWORD (wParam) ; break ; case SB_TOP: GetScrollRange (hwndScroll, SB_CTL, &iFreq, &iDummy) ; break ; case SB_BOTTOM: GetScrollRange (hwndScroll, SB_CTL, &iDummy, &iFreq) ; break ; } iFreq = max (FREQ_MIN, min (FREQ_MAX, iFreq)) ; SetScrollPos (hwndScroll, SB_CTL, iFreq, TRUE) ; SetDlgItemInt (hwnd, IDC_TEXT, iFreq, FALSE) ; return TRUE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDC_ONOFF: // If turning on waveform, hWaveOut is NULL if (hWaveOut == NULL) { // Allocate memory for 2 headers and 2 buffers pWaveHdr1 = (PWAVEHDR)malloc (sizeof (WAVEHDR)) ; pWaveHdr2 = (PWAVEHDR)malloc (sizeof (WAVEHDR)) ; pBuffer1 = (PBYTE)malloc (OUT_BUFFER_SIZE) ; pBuffer2 = (PBYTE)malloc (OUT_BUFFER_SIZE) ; if (!pWaveHdr1 || !pWaveHdr2 || !pBuffer1 || !pBuffer2) { if (!pWaveHdr1) free (pWaveHdr1) ; if (!pWaveHdr2) free (pWaveHdr2) ; if (!pBuffer1) free (pBuffer1) ; if (!pBuffer2) free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Error allocating memory!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return TRUE ; } // Variable to indicate Off button pressed bShutOff = FALSE ; // Open waveform audio for output waveformat.wFormatTag = WAVE_FORMAT_PCM ; waveformat.nChannels = 1 ; waveformat.nSamplesPerSec = SAMPLE_RATE ; waveformat.nAvgBytesPerSec = SAMPLE_RATE ; waveformat.nBlockAlign = 1 ; waveformat.wBitsPerSample = 8 ; waveformat.cbSize = 0 ; if (waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveformat, (DWORD) hwnd, 0, CALLBACK_WINDOW)!= MMSYSERR_NOERROR) { free (pWaveHdr1) ; free (pWaveHdr2) ; free (pBuffer1) ; free (pBuffer2) ; hWaveOut = NULL ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, TEXT ("Error opening waveform audio device!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return TRUE ; } // Set up headers and prepare them pWaveHdr1->lpData = (LPSTR)pBuffer1 ; pWaveHdr1->dwBufferLength = OUT_BUFFER_SIZE ; pWaveHdr1->dwBytesRecorded = 0 ; pWaveHdr1->dwUser = 0 ; pWaveHdr1->dwFlags = 0 ; pWaveHdr1->dwLoops = 1 ; pWaveHdr1->lpNext = NULL ; pWaveHdr1->reserved = 0 ; waveOutPrepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; pWaveHdr2->lpData = (LPSTR)pBuffer2 ; pWaveHdr2->dwBufferLength = OUT_BUFFER_SIZE ; pWaveHdr2->dwBytesRecorded = 0 ; pWaveHdr2->dwUser = 0 ; pWaveHdr2->dwFlags = 0 ; pWaveHdr2->dwLoops = 1 ; pWaveHdr2->lpNext = NULL ; pWaveHdr2->reserved = 0 ; waveOutPrepareHeader (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; } // If turning off waveform, reset waveform audio else { bShutOff = TRUE ; waveOutReset (hWaveOut) ; } return TRUE ; } break ; // Message generated from waveOutOpen call case MM_WOM_OPEN: SetDlgItemText (hwnd, IDC_ONOFF, TEXT ("Turn Off")) ; // Send two buffers to waveform output device FillBuffer (pBuffer1, iFreq) ; waveOutWrite (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; FillBuffer (pBuffer2, iFreq) ; waveOutWrite (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; return TRUE ; // Message generated when a buffer is finished case MM_WOM_DONE: if (bShutOff) { waveOutClose (hWaveOut) ; return TRUE ; } // Fill and send out a new buffer FillBuffer ((PBYTE)((PWAVEHDR) lParam)->lpData, iFreq) ; waveOutWrite (hWaveOut, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ; return TRUE ; case MM_WOM_CLOSE: waveOutUnprepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; waveOutUnprepareHeader (hWaveOut, pWaveHdr2, sizeof (WAVEHDR)) ; free (pWaveHdr1) ; free (pWaveHdr2) ; free (pBuffer1) ; free (pBuffer2) ; hWaveOut = NULL ; SetDlgItemText (hwnd, IDC_ONOFF, TEXT ("Turn On")) ; if (bClosing) EndDialog (hwnd, 0) ; return TRUE ; case WM_SYSCOMMAND: switch (wParam) { case SC_CLOSE: if (hWaveOut != NULL) { bShutOff = TRUE ; bClosing = TRUE ; waveOutReset (hWaveOut) ; } else EndDialog (hwnd, 0) ; return TRUE ; } break ; } return FALSE ; }

SINEWAVE.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Dialog SINEWAVE DIALOG DISCARDABLE 100, 100, 200, 50 STYLE WS_MINIMIZEBOX | WS_VISIBLE | WS_CAPTION | WS_SYSMENU CAPTION "Sine Wave Generator" FONT 8, "MS Sans Serif" BEGIN SCROLLBAR IDC_SCROLL,8,8,150,12 RTEXT "440",IDC_TEXT,160,10,20,8 LTEXT "Hz",IDC_STATIC,182,10,12,8 PUSHBUTTON "Turn On",IDC_ONOFF,80,28,40,14 END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by SineWave.rc #define IDC_STATIC -1 #define IDC_SCROLL 1000 #define IDC_TEXT 1001 #define IDC_ONOFF 1002

注意,FillBuffer常式中用到的OUT_BUFFER_SIZE、SAMPLE_RATE和PI識別字在程式的頂部定義。FillBuffer的iFreq參數是需要的頻率,單位是Hz。還要注意,sin函式的結果調整到了0到254的範圍之間。對於每個樣本,sin函式的fAngle參數都增加一個值,該值的大小是2π弧度乘以需要的頻率再除以取樣頻率。

SINEWAVE的視窗包含三個控制項:一個用於選擇頻率的水平捲動列,一個用於顯示目前所選頻率的靜態文字區域,以及一個標記為「Turn On」的按鈕。按下此按鈕後,您將從連結音效卡的擴音器中聽到正弦波的聲音,同時按鈕上的文字將變成「Turn Off」。用鍵盤或者滑鼠移動捲動列可以改變頻率。要關閉聲音,可以再次按下按鈕。

SINEWAVE程式碼初始化捲動列,以便頻率在WM_INITDIALOG訊息處理期間最低是20Hz,最高是5000Hz。初始化時,捲動列設定為440 Hz。用音樂術語來說就是中音上面的A,它在管絃樂隊演奏時用來調音。DlgProc在接收WM_HSCROLL訊息處理期間改變靜態變數iFreq。注意,Page Left和Page Right將導致DlgProc增加或者減少一個八度音階。

當DlgProc從按鈕收到一個WM_COMMAND訊息時,它首先配置4個記憶體塊-2個用於WAVEHDR結構,我們馬上討論。另兩個用於緩衝區儲存波形資料,我們將這兩個緩衝區稱為pBuffer1和pBuffer2。

通過呼叫waveOutOpen函式,SINEWAVE打開波形聲音設備以便輸出,waveOutOpen函式使用下面的參數:

將第一個參數設定為指向HWAVEOUT(handle to waveform audio output:波形聲音輸出代號)型態的變數。從函式傳回時,此變數將設定為一個代號,後面的波形輸出呼叫中將使用該代號。

waveOutOpen的第二個參數是設備ID。它允許函式可以在安裝多個音效卡的機器上使用。參數的範圍在0到系統所安裝的波形輸出設備數之間。呼叫waveOutGetNumDevs可以獲得波形輸出設備數,而呼叫waveOutGetDevCaps可以找出每個波形輸出設備。如果想消除設備問號,那麼您可以用常數WAVE_MAPPER(定義為-1)來選擇設備,該設備在「控制台」的「多媒體」中「音效」頁面標籤裏的「喜歡使用的裝置」中指定。另外,如果首選設備不能滿足您的需要,而其他設備可以,那麼系統將選擇其他設備。

第三個參數是指向WAVEFORMATEX結構的指標(後面將詳細介紹)。第四個參數是視窗代號或指向動態連結程式庫中callback函式的指標,用來表示接收波形輸出訊息的視窗或者callback函式。使用callback函式時,可在第五個參數中指定程式定義的資料。dwFlags參數可設為CALLBACK_WINDOW或CALLBACK_FUNCTION,以表示第四個參數的型態。您也可用WAVE_FORMAT_QUERY標記來檢查能否打開設備(實際上並不打開它)。還有其他幾個標記可用。

waveOutOpen的第三個參數定義為指向WAVEFORMATEX型態結構的指標,此結構在MMSYSTEM.H中定義如下:

waveOutOpen (&hWaveOut, wDeviceID, &waveformat, dwCallBack, dwCallBackData, dwFlags) ;

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函式傳回MMSYSERR_NOERROR(定義為0),否則傳回非0的錯誤代碼。如果waveOutOpen的傳回值非0,則SINEWAVE清除視窗,並顯示一個標識錯誤的訊息方塊。

現在設備打開了,SINEWAVE繼續初始化兩個WAVEHDR結構的欄位,這兩個結構用於在API中傳遞緩衝。WAVEHDR定義如下:

waveOutOpen ( &hWaveOut, WAVE_MAPPER, &waveformat, (DWORD) hwnd, 0, CALLBACK_WINDOW)

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使用的「雙緩衝」技術避免了聲音的不連續。

當使用者單擊「Turn Off」按鈕關閉聲音時,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、釋放所有的記憶體塊並把按鈕上的文字改回「Turn On」。

如果硬體繼續播放緩衝區的聲音資料,那麼它自己呼叫waveOutClose就沒有作用。您必須先呼叫waveOutReset來停止播放並產生MM_WOM_DONE訊息。當wParam是SC_CLOSE時,DlgProc也處理WM_SYSCOMMAND訊息,這是因為使用者從系統功能表中選擇了「Close」。如果波形聲音繼續播放,DlgProc則呼叫waveOutReset。無論如何,最後總要呼叫EndDialog來結束程式。

數位錄音機

Windows提供了一個稱為「錄音程式」來錄製和播放數位聲音。程式22-3所示的程式(RECORD1)不如「錄音程式」完善,因為它不含有任何檔案I/O,也不允許聲音編輯。然而,這個程式顯示了使用低階波形聲音API來錄製和重播聲音的基本方法。

程式22-3 RECORD1 RECORD1.C /*--------------------------------------------------------------------------- RECORD1.C -- Waveform Audio Recorder (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #pragma comment(lib,"winmm.lib") #define INP_BUFFER_SIZE 16384 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("Record1") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { if (-1 == DialogBox (hInstance, TEXT ("Record"), NULL, DlgProc)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; } return 0 ; } void ReverseMemory (BYTE * pBuffer, int iLength) { BYTE b ; int i ; for (i = 0 ; i < iLength / 2 ; i++) { b = pBuffer [i] ; pBuffer [i] = pBuffer [iLength - i - 1] ; pBuffer [iLength - i - 1] = b ; } } BOOL CALLBACK DlgProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL bRecording, bPlaying, bReverse, bPaused, bEnding, bTerminating ; static DWORD dwDataLength, dwRepetitions = 1 ; static HWAVEIN hWaveIn ; static HWAVEOUT hWaveOut ; static PBYTE pBuffer1, pBuffer2, pSaveBuffer, pNewBuffer ; static PWAVEHDR pWaveHdr1, pWaveHdr2 ; static TCHAR szOpenError[] = TEXT ("Error opening waveform audio!"); static TCHAR szMemError [] = TEXT ("Error allocating memory!") ; static WAVEFORMATEX waveform ; switch (message) { case WM_INITDIALOG: // Allocate memory for wave header pWaveHdr1 = (PWAVEHDR)malloc (sizeof (WAVEHDR)) ; pWaveHdr2 = (PWAVEHDR)malloc (sizeof (WAVEHDR)) ; // Allocate memory for save buffer pSaveBuffer = (PBYTE)malloc (1) ; return TRUE ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDC_RECORD_BEG: // Allocate buffer memory pBuffer1 = (PBYTE)malloc (INP_BUFFER_SIZE) ; pBuffer2 = (PBYTE)malloc (INP_BUFFER_SIZE) ; if (!pBuffer1 || !pBuffer2) { if (pBuffer1) free (pBuffer1) ; if (pBuffer2) free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szMemError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; return TRUE ; } // Open waveform audio for input waveform.wFormatTag = WAVE_FORMAT_PCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec = 11025 ; waveform.nAvgBytesPerSec = 11025 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveInOpen (&hWaveIn, WAVE_MAPPER, &waveform, (DWORD) hwnd, 0, CALLBACK_WINDOW)) { free (pBuffer1) ; free (pBuffer2) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; } // Set up headers and prepare them pWaveHdr1->lpData = (LPSTR)pBuffer1 ; pWaveHdr1->dwBufferLength = INP_BUFFER_SIZE ; pWaveHdr1->dwBytesRecorded = 0 ; pWaveHdr1->dwUser = 0 ; pWaveHdr1->dwFlags = 0 ; pWaveHdr1->dwLoops = 1 ; pWaveHdr1->lpNext = NULL ; pWaveHdr1->reserved = 0 ; waveInPrepareHeader (hWaveIn, pWaveHdr1, sizeof (WAVEHDR)) ; pWaveHdr2->lpData = (LPSTR)pBuffer2 ; pWaveHdr2->dwBufferLength = INP_BUFFER_SIZE ; pWaveHdr2->dwBytesRecorded = 0 ; pWaveHdr2->dwUser = 0 ; pWaveHdr2->dwFlags = 0 ; pWaveHdr2->dwLoops = 1 ; pWaveHdr2->lpNext = NULL ; pWaveHdr2->reserved = 0 ; waveInPrepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; return TRUE ; case IDC_RECORD_END: // Reset input to return last buffer bEnding = TRUE ; waveInReset (hWaveIn) ; return TRUE ; case IDC_PLAY_BEG: // Open waveform audio for output waveform.wFormatTag = WAVE_FORMAT_PCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec = 11025 ; waveform.nAvgBytesPerSec = 11025 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveOutOpen (&hWaveOut, WAVE_MAPPER, &waveform, (DWORD) hwnd, 0, CALLBACK_WINDOW)) { MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; } return TRUE ; case IDC_PLAY_PAUSE: // Pause or restart output if (!bPaused) { waveOutPause (hWaveOut) ; SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Resume")) ; bPaused = TRUE ; } else { waveOutRestart (hWaveOut) ; SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; } return TRUE ; case IDC_PLAY_END: // Reset output for close preparation bEnding = TRUE ; waveOutReset (hWaveOut) ; return TRUE ; case IDC_PLAY_REV: // Reverse save buffer and play bReverse = TRUE ; ReverseMemory (pSaveBuffer, dwDataLength) ; SendMessage (hwnd, WM_COMMAND, IDC_PLAY_BEG, 0) ; return TRUE ; case IDC_PLAY_REP: // Set infinite repetitions and play dwRepetitions = -1 ; SendMessage (hwnd, WM_COMMAND, IDC_PLAY_BEG, 0) ; return TRUE ; case IDC_PLAY_SPEED: // Open waveform audio for fast output waveform.wFormatTag = WAVE_FORMAT_PCM ; waveform.nChannels = 1 ; waveform.nSamplesPerSec =22050 ; waveform.nAvgBytesPerSec= 22050 ; waveform.nBlockAlign = 1 ; waveform.wBitsPerSample = 8 ; waveform.cbSize = 0 ; if (waveOutOpen (&hWaveOut, 0, &waveform, (DWORD) hwnd, 0, CALLBACK_WINDOW)) { MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szOpenError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; } return TRUE ; } break ; case MM_WIM_OPEN: // Shrink down the save buffer pSaveBuffer = (PBYTE)realloc (pSaveBuffer, 1) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), FALSE) ; SetFocus (GetDlgItem (hwnd, IDC_RECORD_END)) ; // Add the buffers waveInAddBuffer (hWaveIn, pWaveHdr1, sizeof (WAVEHDR)) ; waveInAddBuffer (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; // Begin sampling bRecording = TRUE ; bEnding = FALSE ; dwDataLength = 0 ; waveInStart (hWaveIn) ; return TRUE ; case MM_WIM_DATA: // Reallocate save buffer memory pNewBuffer = (PBYTE)realloc ( pSaveBuffer, dwDataLength + ((PWAVEHDR) lParam)->dwBytesRecorded) ; if (pNewBuffer == NULL) { waveInClose (hWaveIn) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox (hwnd, szMemError, szAppName, MB_ICONEXCLAMATION | MB_OK) ; return TRUE ; } pSaveBuffer = pNewBuffer ; CopyMemory (pSaveBuffer + dwDataLength, ((PWAVEHDR) lParam)->lpData, ((PWAVEHDR) lParam)->dwBytesRecorded) ; dwDataLength += ((PWAVEHDR) lParam)->dwBytesRecorded ; if (bEnding) { waveInClose (hWaveIn) ; return TRUE ; } // Send out a new buffer waveInAddBuffer (hWaveIn, (PWAVEHDR) lParam, sizeof (WAVEHDR)) ; return TRUE ; case MM_WIM_CLOSE: // Free the buffer memory waveInUnprepareHeader (hWaveIn, pWaveHdr1, sizeof (WAVEHDR)) ; waveInUnprepareHeader (hWaveIn, pWaveHdr2, sizeof (WAVEHDR)) ; free (pBuffer1) ; free (pBuffer2) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE) ; SetFocus (GetDlgItem (hwnd, IDC_RECORD_BEG)) ; if (dwDataLength > 0) { EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), TRUE) ; SetFocus (GetDlgItem (hwnd, IDC_PLAY_BEG)) ; } bRecording = FALSE ; if (bTerminating) SendMessage (hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L) ; return TRUE ; case MM_WOM_OPEN: // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), TRUE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), FALSE) ; SetFocus (GetDlgItem (hwnd, IDC_PLAY_END)) ; // Set up header pWaveHdr1->lpData = (LPSTR)pSaveBuffer ; pWaveHdr1->dwBufferLength = dwDataLength ; pWaveHdr1->dwBytesRecorded = 0 ; pWaveHdr1->dwUser = 0 ; pWaveHdr1->dwFlags = WHDR_BEGINLOOP | WHDR_ENDLOOP ; pWaveHdr1->dwLoops = dwRepetitions ; pWaveHdr1->lpNext = NULL ; pWaveHdr1->reserved = 0 ; // Prepare and write waveOutPrepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; waveOutWrite (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; bEnding = FALSE ; bPlaying = TRUE ; return TRUE ; case MM_WOM_DONE: waveOutUnprepareHeader (hWaveOut, pWaveHdr1, sizeof (WAVEHDR)) ; waveOutClose (hWaveOut) ; return TRUE ; case MM_WOM_CLOSE: // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REV), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_REP), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_SPEED), TRUE) ; SetFocus (GetDlgItem (hwnd, IDC_PLAY_BEG)) ; SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; dwRepetitions = 1 ; bPlaying = FALSE ; if (bReverse) { ReverseMemory (pSaveBuffer, dwDataLength) ; bReverse = FALSE ; } if (bTerminating) SendMessage (hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L) ; return TRUE ; case WM_SYSCOMMAND: switch (LOWORD (wParam)) { case SC_CLOSE: if (bRecording) { bTerminating = TRUE ; bEnding = TRUE ; waveInReset (hWaveIn) ; return TRUE ; } if (bPlaying) { bTerminating = TRUE ; bEnding = TRUE ; waveOutReset (hWaveOut) ; return TRUE ; } free (pWaveHdr1) ; free (pWaveHdr2) ; free (pSaveBuffer) ; EndDialog (hwnd, 0) ; return TRUE ; } break ; } return FALSE ; }

RECORD.RC (摘錄) //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 #define IDC_RECORD_BEG 1000 #define IDC_RECORD_END 1001 #define IDC_PLAY_BEG 1002 #define IDC_PLAY_PAUSE 1003 #define IDC_PLAY_END 1004 #define IDC_PLAY_REV 1005 #define IDC_PLAY_REP 1006 #define IDC_PLAY_SPEED 1007

RECORD.RC和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.025 kHz的取樣頻率和8位元的取樣精確度來錄音和放音,而不管設備能否提供更高的取樣頻率和取樣精確度。唯一的例外是加速功能,加速時RECORD1按22.050kHz的取樣頻率播放聲音,這樣不僅播放速度提高了一倍,而且頻率也提高了一個音階。

錄製聲音既包括為輸入而打開波形聲音硬體,還包括將緩衝區傳遞給API,以便接收聲音資料。

RECORD1設有幾個記憶體塊。其中三個很小,至少在初始化時很小,並且在DlgProc的WM_INITDIALOG訊息處理期間進行配置。程式配置兩個WAVEHDR結構,分別由指標pWaveHdr1和pWaveHdr2指向。這兩個結構用於將緩衝區傳遞給波形API。pSaveBuffer指標指向儲存整個錄音的緩衝區,最初配置時只有一個位元組。然後,隨著錄音的進行,該緩衝區不斷增大,以適應所有的聲音資料(如果錄音時間過長,則RECORD1能夠在錄製程序中及時發現記憶體溢出,並允許您重放成功儲存的聲音)。由於這個緩衝區用來儲存堆積的聲音資料,所以我將其稱為「儲存緩衝區(save buffer)」。指標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也提供高階媒體控制介面(Media Control Interface)。

對波形聲音來說,低階介面與MCI之間的主要區別在於MCI用波形檔案記錄聲音資料,並通過讀取檔案來播放聲音。由於在播放聲音之前要讀取檔案、處理檔案然後再寫入檔案,所以讓RECODE1來實作「特殊效果」很困難。這是典型的折衷選擇問題:功能齊全或是使用方便?低階介面很靈活,但MCI(其中的大部分)更方便。

MCI有兩種不同但又相關的實作形式。一種形式用訊息和資料結構將命令發送給多媒體設備,然後再從那裏接收資訊。另一種形式使用ASCII文字字串。建立文字命令的介面最初是為了讓多媒體設備接受簡單的描述命令語言的控制。但它也提供非常容易的交談式控制,請參見本章前面,TESTMCI程式的展示。

RECORD2程式,如程式22-4所示,使用MCI形式的訊息和資料結構來實作另一個數位聲音錄音機和播放器。雖然它使用的對話方塊模板與RECORD1一樣,但並沒有實作三個特殊效果的按鈕。

程式22-4 RECORD2 RECORD2.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 BOOL bRecording, bPlaying, bPaused ; static TCHAR szFileName[] = TEXT ("record2.wav") ; static WORD wDeviceID ; DWORD dwError ; MCI_GENERIC_PARMS mciGeneric ; MCI_OPEN_PARMS mciOpen ; MCI_PLAY_PARMS mciPlay ; MCI_RECORD_PARMS mciRecord ; MCI_SAVE_PARMS mciSave ; switch (message) { case WM_COMMAND: switch (wParam) { case IDC_RECORD_BEG: // Delete existing waveform file DeleteFile (szFileName) ; // Open waveform audio mciOpen.dwCallback = 0 ; mciOpen.wDeviceID = 0 ; mciOpen.lpstrDeviceType = TEXT ("waveaudio") ; mciOpen.lpstrElementName = TEXT ("") ; mciOpen.lpstrAlias = NULL ; dwError = mciSendCommand (0, MCI_OPEN, MCI_WAIT | MCI_OPEN_TYPE | MCI_OPEN_ELEMENT, (DWORD) (LPMCI_OPEN_PARMS) &mciOpen) ; if (dwError != 0) { ShowError (hwnd, dwError) ; return TRUE ; } // Save the Device ID wDeviceID = mciOpen.wDeviceID ; // Begin recording mciRecord.dwCallback = (DWORD) hwnd ; mciRecord.dwFrom = 0 ; mciRecord.dwTo = 0 ; mciSendCommand (wDeviceID, MCI_RECORD, MCI_NOTIFY, (DWORD) (LPMCI_RECORD_PARMS) &mciRecord) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC_RECORD_END)) ; bRecording = TRUE ; return TRUE ; case IDC_RECORD_END: // Stop recording mciGeneric.dwCallback = 0 ; mciSendCommand (wDeviceID, MCI_STOP, MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) &mciGeneric) ; // Save the file mciSave.dwCallback = 0 ; mciSave.lpfilename = szFileName ; mciSendCommand (wDeviceID, MCI_SAVE, MCI_WAIT | MCI_SAVE_FILE, (DWORD) (LPMCI_SAVE_PARMS) &mciSave) ; // Close the waveform device mciSendCommand (wDeviceID, MCI_CLOSE, MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) &mciGeneric) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC_PLAY_BEG)) ; bRecording = FALSE ; return TRUE ; case IDC_PLAY_BEG: // Open waveform audio mciOpen.dwCallback = 0 ; mciOpen.wDeviceID = 0 ; mciOpen.lpstrDeviceType = NULL ; mciOpen.lpstrElementName = szFileName ; mciOpen.lpstrAlias = NULL ; dwError = mciSendCommand ( 0, MCI_OPEN, MCI_WAIT | MCI_OPEN_ELEMENT, (DWORD) (LPMCI_OPEN_PARMS) &mciOpen) ; if (dwError != 0) { ShowError (hwnd, dwError) ; return TRUE ; } // Save the Device ID wDeviceID = mciOpen.wDeviceID ; // Begin playing mciPlay.dwCallback = (DWORD) hwnd ; mciPlay.dwFrom = 0 ; mciPlay.dwTo = 0 ; mciSendCommand (wDeviceID, MCI_PLAY, MCI_NOTIFY, (DWORD) (LPMCI_PLAY_PARMS) &mciPlay) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), TRUE) ; SetFocus (GetDlgItem (hwnd, IDC_PLAY_END)) ; bPlaying = TRUE ; return TRUE ; case IDC_PLAY_PAUSE: if (!bPaused) // Pause the play { mciGeneric.dwCallback = 0 ; mciSendCommand (wDeviceID, MCI_PAUSE, MCI_WAIT, (DWORD) (LPMCI_GENERIC_PARMS) & mciGeneric); SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Resume")) ; 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函式呼叫,其中最重要的呼叫如下所示:

第一個參數是設備的識別數字(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值,否則傳回錯誤代碼。要向使用者報告此錯誤,可用下面的函式獲得描述錯誤的文字字串:

此函式在程式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,其語法如下:

其中唯一的參數是MCI命令字串。函式傳回布林值-如果呼叫成功,則傳回非0值,否則傳回0。在功能上,mciExecute函式相同於呼叫後三個參數為NULL或0的mciSendString(TESTMCI中使用的依據字串的MCI函式),然後在發生錯誤時呼叫mciGetErrorString和MessageBox。

雖然mciExecute不再是API的一部分,但我還是在RECORD3版的數位錄音機中使用了這個函式。和RECORD2一樣,RECORD3程式也使用RECORD1中的資源描述檔RECORD.RC和RESOURCE.H,如程式22-5所示。

error = mciSendCommand (wDeviceID, message, dwFlags, dwParam)

mciGetErrorString (error, szBuffer, dwLength)

bSuccess = mciExecute (szCommand) ;

程式22-5 RECORD3 RECORD3.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 ; TCHAR szErrorStr [1024] ; if (error = mciSendString (szCommand, NULL, 0, NULL)) { mciGetErrorString (error, szErrorStr, sizeof (szErrorStr) / sizeof (TCHAR)) ; MessageBeep (MB_ICONEXCLAMATION) ; MessageBox ( NULL, szErrorStr, TEXT ("MCI Error"), MB_OK | MB_ICONEXCLAMATION) ; } return error == 0 ; } BOOL CALLBACK DlgProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BOOL bRecording, bPlaying, bPaused ; switch (message) { case WM_COMMAND: switch (wParam) { case IDC_RECORD_BEG: // Delete existing waveform file DeleteFile (TEXT ("record3.wav")) ; // Open waveform audio and record if (!mciExecute (TEXT ("open new type waveaudio alias mysound"))) return TRUE ; mciExecute (TEXT ("record mysound")) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC_RECORD_END)) ; bRecording = TRUE ; return TRUE ; case IDC_RECORD_END: // Stop, save, and close recording mciExecute (TEXT ("stop mysound")) ; mciExecute (TEXT ("save mysound record3.wav")) ; mciExecute (TEXT ("close mysound")) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC_PLAY_BEG)) ; bRecording = FALSE ; return TRUE ; case IDC_PLAY_BEG: // Open waveform audio and play if (!mciExecute (TEXT ("open record3.wav alias mysound"))) return TRUE ; mciExecute (TEXT ("play mysound")) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), TRUE) ; SetFocus (GetDlgItem (hwnd, IDC_PLAY_END)) ; bPlaying = TRUE ; return TRUE ; case IDC_PLAY_PAUSE: if (!bPaused) // Pause the play { mciExecute (TEXT ("pause mysound")) ; SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Resume")) ; bPaused = TRUE ; } else // Begin playing again { mciExecute (TEXT ("play mysound")) ; SetDlgItemText (hwnd, IDC_PLAY_PAUSE, TEXT ("Pause")) ; bPaused = FALSE ; } return TRUE ; case IDC_PLAY_END: // Stop and close mciExecute (TEXT ("stop mysound")) ; mciExecute (TEXT ("close mysound")) ; // Enable and disable buttons EnableWindow (GetDlgItem (hwnd, IDC_RECORD_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_RECORD_END), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_BEG), TRUE) ; EnableWindow (GetDlgItem (hwnd, IDC_PLAY_PAUSE), FALSE); EnableWindow (GetDlgItem (hwnd, IDC_PLAY_END), FALSE); SetFocus (GetDlgItem (hwnd, IDC_PLAY_BEG)) ; bPlaying = FALSE ; bPaused = FALSE ; return TRUE ; } break ; case WM_SYSCOMMAND: switch (wParam) { case SC_CLOSE: if (bRecording) SendMessage (hwnd, WM_COMMAND, IDC_RECORD_END, 0L); if (bPlaying) SendMessage (hwnd, WM_COMMAND, IDC_PLAY_END, 0L) ; EndDialog (hwnd, 0) ; return TRUE ; } break ; } return FALSE ; }

在研究訊息導向和文字導向的MCI介面時,您會發現它們非常相近。很容易就可以猜測出MCI將命令字串轉換為相應的命令訊息和資料結構。RECORD3可以使用像RECORD2一樣使用MM_MCINOTIFY訊息,但是它沒有選擇mciExecute函式的好處,它的缺點是程式不知道什麼時候播放完波形檔案。因此,這些按鈕不能自動改變狀態。您必須人工按下「End」按鈕,以便讓程式知道它已經準備再次錄音或播放。

注意MCI的open命令中alias關鍵字的用法。它允許所有後來的MCI命令使用別名來引用設備。

波形聲音檔案格式

如果在十六進位轉儲程式下研究未壓縮的.WAV檔案(即PCM),您會發現它們具有表22-1所示的格式。

表22-1 .WAV檔案格式

這是一種擴充自RIFF(Resource Interchange File Format:資源交換檔案格式)的格式。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和44 100個樣本。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比例依此類推。

對於德國科學家Hermann Helmholtz(1821-1894),這是瞭解音質的關鍵。在他的名著《On the Sensations of Tone》(1885年,1954年由Dover Press再版)中,Helmholtz假定耳朵和大腦將複雜的聲音分解為正弦波,而這些正弦波相關的強度就是我們所感受的音質。不幸的是,事情還沒有這麼簡單。

隨著1968年Wendy Carlos的唱片《Switched on Bach》的發佈,電子音樂合成器引起了公眾的廣泛注意。那時使用的合成器(例如Moog)是類比合成器。這些合成器使用類比電路來產生各種聲音波形,例如方波、三角波形和鋸齒波形。要使這些波形聽起來更像真實的樂器,它們取決於單個音符的變化程序。波形的所有振幅以「包絡(envelope)」形成。當音符開始時,振幅由0開始增加,通常增加非常快。這就是所謂的起奏。然後當音符持續時,振幅保持為常數,這時稱為持續。音符結束時,振幅降為0,這時稱為釋放。

波形通過濾波器,濾波器將削弱一些諧波,並將簡單波形轉換得更複雜、更有樂感。這些濾波器的切斷頻率由包絡控制,以便聲音的諧波內容在音符的程序中改變。

因為這些合成器以豐富的波形格式調和開始,而且一些諧波通過濾波器進行了削弱,這種形式的合成稱為「負合成」。

即使在負合成期間,許多人也還會在電子音樂中發現疊加合成是下一個大問題。

在疊加合成中,您可以從許多整數倍正弦波生成器開始,選擇整數倍以便於每個正弦波都對應一個諧波。每個諧波的振幅都由一個包絡單獨控制。使用類比電路的疊加合成不實用,因為對單個音符就需要8和24之間數目的正弦波生成器,而與這些正弦波生成器相關的頻率必須精確的互相對齊。類比波形生成器穩定性很差,而且容易發生頻率漂移。

不過,由數位合成器(可以數位化地使用對照表產生波形)和電腦產生的波形,頻率漂移並不是個問題,因而疊加合成也就切實可行了。因此總的來說:在錄製真實的樂曲時,可以用Fourier分解法將其分解成多個諧波。然後就可以確定每個諧波的相對強度,再用多個正弦波數位化地產生聲音。

如果開始實驗時用Fourier分析法分析實際的音調,並從多個正弦波來產生這些音調,那麼人們將發現音質並不像Helmholtz所認為的那樣簡單。

最大的問題是真實音調的諧波之間並沒有精確的整數關係。事實上,「諧波」一詞對於實際的音調來說並不十分適當。各種正弦波組成都不和諧,或者更準確地說是「泛音」。

人們發現,實際音調泛音之間的不和諧在創造「真實的」聲音時很重要。靜態和諧會產生「電流」聲。每個泛音都在單個音符上改變振幅和頻率。泛音中,相對頻率和振幅的關係對於不同的泛音以及來自相同樂器的不同強度是不同的。實際音調中最複雜的部分發生在音符的起奏部分,這時比較不和諧。人們發現音符的這個複雜的起奏位置對於人類感受音質很重要。

簡而言之,實際樂器的聲音比任何想像的都更複雜。分析音調的觀點,以及後面用於控制泛音的振幅和頻率的相對簡單的包絡觀點顯然都不實用。

實際樂曲的一些分析法發表於早期(1977到1978年間)的《Computer Music Journal》(當時由People's Computer Company發行,現在由MIT Press發行)由James A. Moorer、John Grey和John Strawn Some編寫了第三部分叢書《Lexicon of Analyzed Tones》,該書顯示了在小提琴、雙簧管、單簧管和喇叭上演奏一個音符(小於半秒種)的泛音的振幅和頻率圖形。所用的音符是中音C上的降E。小提琴用20個泛音,雙簧管和單簧管用21個,而喇叭用12個。實際上,《Computer Music Journal》的Volume II、Number 2(1978年9月)包含了用線段來近似雙簧管、單簧管和喇叭的不同頻率和振幅的包絡。

因此,利用Windows上支援的聲音波形功能,下面的程序很簡單:將這些數字鍵入程式、為每個泛音都產生多個正弦波樣本、添加這些樣本並將其發送給波形聲音音效卡,因此把20年前原始錄製的聲音重新製造出來也很容易。ADDSYNTH(「疊加合成」)如程式22-6所示。

程式22-6 ADDSYNTH ADDSYNTH.C /*-------------------------------------------------------------------------- ADDSYNTH.C -- Additive Synthesis Sound Generation (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <math.h> #include "addsynth.h" #include "resource.h" #define ID_TIMER 1 #define SAMPLE_RATE 22050 #define MAX_PARTIALS 21 #define PI 3.14159 BOOL CALLBACK DlgProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName [] = TEXT ("AddSynth") ; // Sine wave generator // ------------------- double SineGenerator (double dFreq, double * pdAngle) { double dAmp ; dAmp = sin (* pdAngle) ; * pdAngle += 2 * PI * dFreq / SAMPLE_RATE ; if (* pdAngle >= 2 * PI) * pdAngle -= 2 * PI ; return dAmp ; } // Fill a buffer with composite waveform // ------------------------------------- VOID FillBuffer (INS ins, PBYTE pBuffer, int iNumSamples) { static double dAngle [MAX_PARTIALS] ; double dAmp, dFrq, dComp, dFrac ; int i, iPrt, iMsecTime, iCompMaxAmp, iMaxAmp, iSmp ; // Calculate the composite maximum amplitude iCompMaxAmp = 0 ; for (iPrt = 0 ; iPrt < ins.iNumPartials ; iPrt++) { iMaxAmp = 0 ; for (i = 0 ; i < ins.pprt[iPrt].iNumAmp ; i++) iMaxAmp = max (iMaxAmp, ins.pprt[iPrt].pEnvAmp[i].iValue) ; iCompMaxAmp += iMaxAmp ; } // Loop through each sample for (iSmp = 0 ; iSmp < iNumSamples ; iSmp++) { dComp = 0 ; iMsecTime = (int) (1000 * iSmp / SAMPLE_RATE) ; // Loop through each partial for (iPrt = 0 ; iPrt < ins.iNumPartials ; iPrt++) { dAmp = 0 ; dFrq = 0 ; for (i = 0 ; i < ins.pprt[iPrt].iNumAmp - 1 ; i++) { if (iMsecTime >= ins.pprt[iPrt].pEnvAmp[i ].iTime && iMsecTime <= ins.pprt[iPrt].pEnvAmp[i+1].iTime) { dFrac = (double) (iMsecTime - ins.pprt[iPrt].pEnvAmp[i ].iTime) / (ins.pprt[iPrt].pEnvAmp[i+1].iTime - ins.pprt[iPrt].pEnvAmp[i ].iTime) ; dAmp = dFrac * ins.pprt[iPrt].pEnvAmp[i+1].iValue + (1-dFrac) * ins.pprt[iPrt].pEnvAmp[i ].iValue ; break ; } } for (i = 0 ; i < ins.pprt[iPrt].iNumFrq - 1 ; i++) { if (iMsecTime >= ins.pprt[iPrt].pEnvFrq[i ].iTime && iMsecTime <= ins.pprt[iPrt].pEnvFrq[i+1].iTime) { dFrac = (double) (iMsecTime -ins.pprt[iPrt].pEnvFrq[i ].iTime) / (ins.pprt[iPrt].pEnvFrq[i+1].iTime - ins.pprt[iPrt].pEnvFrq[i ].iTime) ; dFrq = dFrac * ins.pprt[iPrt].pEnvFrq[i+1].iValue + (1-dFrac) * ins.pprt[iPrt].pEnvFrq[i ].iValue ; break ; } } dComp += dAmp * SineGenerator (dFrq, dAngle + iPrt) ; } pBuffer[iSmp] = (BYTE) (127 + 127 * dComp / iCompMaxAmp) ; } } // Make a waveform file // ------------------------------------------------------------------------- BOOL MakeWaveFile (INS ins, TCHAR * szFileName) { DWORD dwWritten ; HANDLE hFile ; int iChunkSize, iPcmSize, iNumSamples ; PBYTE pBuffer ; WAVEFORMATEX waveform ; hFile = CreateFile (szFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL) ; if (hFile == NULL) return FALSE ; iNumSamples = ((long) ins.iMsecTime * SAMPLE_RATE / 1000 + 1) / 2 * 2 ; iPcmSize = sizeof (PCMWAVEFORMAT) ; iChunkSize = 12 + iPcmSize + 8 + iNumSamples ; if (NULL == (pBuffer = 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 #define IDC_TRUMPET 1000 #define IDC_OBOE 1001 #define IDC_CLARINET 1002 #define IDC_TEXT 1003

這裏沒有給出附加檔案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.05 kHz的取樣頻率下,每22個樣本對應於相同的毫秒時間值)。然後,FillBuffer在泛音中迴圈。對於頻率和振幅,它找出與毫秒時間值對應的包絡線段,並執行線性插補。

頻率值與相位角值一起傳遞給SineGenerator函式。本章前面討論過,產生數位化的正弦波形需要保持相位角值,並依據頻率值增加。從SineGenerator函式傳回時,正弦值將乘以泛音的振幅並累加。樣本的所有泛音都加在起來之後,樣本就縮放到位元組大小。

起床號波形聲音

WAKEUP,如程式22-7所示,是原始碼檔案看起來不是很完整的程式之一。程式視窗看起來像對話方塊,但是沒有資源描述檔(我們已經知道如何編寫),並且程式使用一個波形檔案,但在光碟上卻沒有這樣的檔案。不過,程式非常有趣:它播放的聲音很大,並且非常令人討厭。WAKEUP是我的鬧鐘,能夠喚醒我繼續工作。

程式22-7 WAKEUP WAKEUP.C /*--------------------------------------------------------------------------- WAKEUP.C -- Alarm Clock Program (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include <commctrl.h> // ID values for 3 child windows #define ID_TIMEPICK 0 #define ID_CHECKBOX 1 #define ID_PUSHBTN 2 // Timer ID #define ID_TIMER 1 // Number of 100-nanosecond increments (ie FILETIME ticks) in an hour #define FTTICKSPERHOUR (60 * 60 * (LONGLONG) 10000000) // Defines and structure for waveform "file" #define SAMPRATE 11025 #define NUMSAMPS (3 * SAMPRATE) #define HALFSAMPS (NUMSAMPS / 2) typedef struct { char chRiff[4] ; DWORD dwRiffSize ; char chWave[4] ; char chFmt [4] ; DWORD dwFmtSize ; PCMWAVEFORMAT pwf ; char chData[4] ; DWORD dwDataSize ; BYTE byData[0] ; } WAVEFORM ; // The window proc and the subclass proc LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; LRESULT CALLBACK SubProc (HWND, UINT, WPARAM, LPARAM) ; // Original window procedure addresses for the subclassed windows WNDPROC SubbedProc [3] ; // The current child window with the input focus HWND hwndFocus ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInst, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName [] = TEXT ("WakeUp") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = 0 ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) (1 + COLOR_BTNFACE) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, szAppName, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HWND hwndDTP, hwndCheck, hwndPush ; static WAVEFORM waveform = { "RIFF", NUMSAMPS + 0x24, "WAVE", "fmt ", sizeof (PCMWAVEFORMAT), 1, 1, SAMPRATE, SAMPRATE, 1, 8, "data", NUMSAMPS } ; static WAVEFORM * pwaveform ; FILETIME ft ; HINSTANCE hInstance ; INITCOMMONCONTROLSEX icex ; int i, cxChar, cyChar ; LARGE_INTEGER li ; SYSTEMTIME st ; switch (message) { case WM_CREATE: // Some initialization stuff hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; icex.dwSize = sizeof (icex) ; icex.dwICC = ICC_DATE_CLASSES ; InitCommonControlsEx (&icex) ; // Create the waveform file with alternating square waves pwaveform = malloc (sizeof (WAVEFORM) + NUMSAMPS) ; * pwaveform = waveform ; for (i = 0 ; i < HALFSAMPS ; i++) if (i % 600 < 300) if (i % 16 < 8) pwaveform->byData[i] = 25 ; else pwaveform->byData[i] = 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-Time Picker」的通用控制項。這個控制項用來讓使用者選擇指定的日期和時間(WAKEUP只使用時間挑選功能)。程式可以使用SYSTEMTIME結構來獲得和設定時間,在獲得和設定PC自身時鐘時也使用該結構。要多方面瞭解Date-Time Picker,請試著建立不帶有任何DTS樣式旗標的視窗。

注意WM_CREATE訊息結束時的處理方式:程式假定您在睡覺之前執行它,並希望它在8小時之後來喚醒您。

現在很明顯,可以從GetLocalTime函式在SYSTEMTIME結構獲得目前時間,而且可以「手工」增加時間。但在一般情況下,此計算將涉及檢查大於24小時的結果時間,這意味著您必須增加天數欄位,然後可能涉及增加月(因此還必須有用於每月天數和閏年檢查的邏輯),最後您可能還要增加年。

事實上,推薦的方法(來自/Platform SDK/Windows Base Services/General Library/Time/Time Reference/Time Structures/SYSTEMTIME)是將SYSTEMTIME轉換為FILETIME結構(使用SystemTimeToFileTime),將FILETIME結構強制轉換為LARGE_INTEGER結構,在大整數上執行計算,再強制轉換回FILETIME結構,然後轉換回SYSTEMTIME結構(使用FileTimeToSystemTime)。

顧名思義,FILETIME結構用於獲得和設定檔案最後一次更新的時間。此結構如下:

type struct _FILETIME // ft { DWORD dwLowDateTime ; DWORD dwHighDateTime ; } FILETIME ;

這兩個欄位一起表示了從1601年1月1日起每隔1000億分之一秒所顯示的64位元值。

Microsoft C/C++編譯器支援64位元整數作為ANSI C的非標準延伸語法。資料型態是__int64。您可以對__int64型態執行所有的常規算術運算,並且有一些執行時期程式庫函式也支援它們。Windows的WINNT.H表頭檔案定義如下:

在Windows中,這有時稱為「四字組」,或者更普遍地稱為「大整數」。也有一個union定義如下:

typedef __int64 LONGLONG ; typedef unsigned __int64 DWORDLONG ;

typedef union _LARGE_INTEGER { struct { DWORD LowPart ; LONG HighPart ; } ; LONGLONG QuadPart ; } LARGE_INTEGER ;

這是/Platform SDK/Windows Base Services/General Library/Large Integer Operations中的全部文件。此union允許您使用32位元或者64位元的大整數。

由電子音樂合成器製造者協會在19世紀80年代早期開發了「樂器數位化介面」(MIDI:Musical Instrument Digital Interface)。MIDI是用於將它們中的電子樂器與電腦連結起來的協定,也是電子音樂領域中相當重要的標準。MIDI規範由MIDI Manufacturers Association(MMA)維護,它的網站是 http://www.midi.org

使用MIDI

MIDI為透過電纜來傳遞數位命令定義了傳輸協定。MIDI電纜使用5針DIN接頭,但是只使用了三個接頭。一個是遮罩,一個是迴路,而第三個傳輸資料。MIDI協定在每秒31,250位元的速度下是單向的。資料的每個位元組都由一個開始位元開始,以一個停止位元結束,用於每秒3,125位元組的有效傳輸速率。

重要的是要瞭解真實的聲音-不論是類比格式還是數位格式-不是經由MIDI電纜傳輸的。通過電纜傳輸的通常都是簡單的命令訊息,長度一般是1、2或3位元組。

簡單的MIDI設定可以包括兩片MIDI相容硬體。一個是本身不發聲,但是單獨產生MIDI訊息的MIDI鍵盤。此鍵盤有一個有標記有「MIDI Out」的MIDI埠。用MIDI電纜將這個埠與MIDI聲音合成器的「MIDI In」埠連結起來。合成器看起來很像前面有幾個按鈕的小盒子。

按下鍵盤上的一個鍵時(假定是中音C),鍵盤就將3個位元組發送給MIDI Out埠。在十六進位中,這些位元組是:

第一個位元組(90)顯示Note On訊息。第二個位元組是鍵號,其中3C是中音C。第三個位元組是敲按鍵的速度,此速度範圍是從1到127。我們恰巧使用了一個對速度不敏感的鍵盤,因此它發送平均速度值。這個3位元組的訊息順著MIDI電纜進入合成器的Midi In埠。通過播放中音C的音調來回應合成器。

釋放鍵時,鍵盤會將另一個3位元組訊息發送給MIDI Out埠:

這與Note On命令相同,但帶有0速位元組。這個位元組值0表示Note Off命令,意味著應該關閉音符。合成器通過停止聲音來回應。

如果合成器有複調音樂的能力(即,同時播放多個音符的能力),那麼您就可以在鍵盤上演奏和絃。鍵盤產生多條Note On訊息,並且合成器將播放所有的音符。當您釋放和絃時,鍵盤就將多條Note Off訊息發送給合成器。

一般來說,這種設定中的鍵盤稱為「MIDI控制器」,它負責產生MIDI訊息來控制合成器。MIDI控制器看起來不像鍵盤。MIDI控制器包括下面幾種:看起來像單簧管或薩克斯管的MID管樂控制器、MIDI吉他控制器、MIDI絃樂控制器和MIDI鼓控制器。至少所有這些控制器都產生3位元組的Note On和Note Off訊息。

勝過類似的鍵盤或傳統樂器,控制器也可以是「編曲器」,它是在記憶體中儲存Note On和Note Off訊息順序,然後再播放的硬體。現在單機編曲器已經比幾年前少見多了,因為它們已經被電腦所替代。安裝MIDI卡的電腦也可以生成Note On和Note Off訊息來控制合成器。MIDI編輯軟體,允許您在螢幕上作曲,還可以儲存來自MIDI控制器的MIDI訊息,並處理這些訊息,然後將MIDI訊息發送給合成器。

合成器有時也稱為「聲音模組(sound module)」或「音源器(tone generator)」。MIDI不指定如何真正產生這些聲音的方法。合成器可以使用任何一種聲音生成技術。

實際上,只有非常簡單的MIDI控制器(例如管樂控制器)才只有MIDI Out電纜埠。通常鍵盤都有內建合成器,並且有三個MIDI電纜埠,分別標記為「MIDI In」、「MIDI Out」和「MIDI Thru」。MIDI In埠接受MIDI訊息,從而播放鍵盤的內部合成器。MIDI Out埠將MIDI訊息從鍵盤發送到外部合成器。MIDI Thru埠是一個輸出埠,它複製MIDI In埠的輸入信號-無論從MIDI In埠獲得什麼都發送給MIDI Thru埠(MIDI Thru埠不包括從MIDI Out埠發送的任何資訊)。

透過電纜連結MIDI硬體只有兩種方法:將一個硬體上的MIDI Out連結到另一個的MIDI In,或者將MIDI Thru與MIDI In連結。MIDI Thru埠允許連結一系列的MIDI合成器。

程式更改

合成器能製作哪種聲音?是鋼琴聲、小提琴聲、喇叭聲還是飛碟聲?通常合成器能夠生成的各種聲音都儲存在ROM或者其他地方。它們通常稱為「聲音」、「樂器」或者「音色」。( 「音色」一詞來自類比合成器的時代,當時通過將音色和絃插入合成器前面的插孔中來設定不同的聲音)。

在MIDI中,合成器能夠生成的各種聲音稱為「程式」。改變這個程式需要向合成器發送MIDI Program Change訊息

其中,pp的範圍是0到127。通常MIDI鍵盤的頂部是一系列有限的按鈕,這些按鈕將產生Program Change訊息。透過按下這些按鈕,您可以從鍵盤控制合成器的聲音。這些按鈕號通常由1開始,而不是由0開始,因此程式代號1與Program Change位元組的0對應。

MIDI規格沒有說明程式代號與樂器的對應關係。例如,著名的Yamaha DX7合成器上的前三個程式分別稱為「Warm Strings」、「 Mellow Horn」和「Pick Guitar」。而在Yamaha TX81Z音調發生器上,它們是Grand Piano、Upright Piano和Deep Grand。在Roland MT-32聲音模組上,它們是Acoustic Piano 1、Acoustic Piano 2和Acoustic Piano 3。因此,如果不希望在從鍵盤製作程式改變時感到吃驚,那麼最好瞭解一下樂器聲與您將使用的合成器的程式代號的對應關係。

這對於包含Program Change訊息的MIDI檔案來說是一個實際問題-這些檔案並不是裝置無關的,因為它們的內容在不同的合成器上聽起來是不一樣的。然而,在最近幾年,「General MIDI」(GM)標準已經把這些程式代號標準化。Windows支援General MIDI。如果合成器與General MIDI規格不一致,那麼程式轉換可使它模擬General MIDI合成器。

MIDI通道

迄今為止,我已經討論了兩條MIDI訊息,第一條是Note On:

其中,kk是鍵號(0到127),v v是速度(0到127)。0速度表示Note Off命令。第二條是Program Change:

其中,pp的範圍是從0到127。這些是典型的MIDI訊息。第一個位元組稱作「狀態」位元組。根據位元組的狀態,它通常後跟0、1或2位元組的「資料」(我即將說明的「系統專有」訊息除外)。從資料位元組中分辨出狀態位元組很容易:高位總是1用於狀態位元組,0用於資料位元組。

然而,我還沒有討論過這兩個訊息的普通格式。Note On訊息的普通格式如下:

而Program Change是:

在這兩種情況下,n表示狀態位元組的低四位元,其變化範圍是0到15。這就是MIDI「通道」。通道一般從1開始編號,因此,如果n為0,則代表通道1。

使用16個不同通道允許一條MIDI電纜傳輸16種不同聲音的訊息。通常,您將發現MIDI訊息的特殊字串以Program Change訊息開始,為所用的不同通道設定聲音,而字串的後面是多條Note On和Note Off命令。再後面可能是其他的Program Change命令。但任何時候,每個通道都只與一種聲音聯繫。

讓我們作一個簡單範例:假定我已經討論過的鍵盤控制能夠同時產生用於兩條不同通道-通道1和通道2-的MIDI訊息。透過按下鍵盤上的按鈕將兩條Program Change訊息發送給合成器:

現在設定通道1用於程式2,並設定通道2用於程式6(回憶通道代號和程式代號都是基於1的,但訊息中的編碼是基於0的)。現在按下鍵盤上的鍵時,就發送兩條Note On訊息,一條用於一個通道:

這就允許您和諧地同時播放兩種樂器的聲音。

另一種方法是「分開」鍵盤。低鍵可以在通道1上產生Note On訊息,高鍵可以在通道2上產生Note On訊息。這就允許您在一個鍵盤上獨立播放兩種樂器的聲音。

當您考慮PC上的MIDI編曲軟體時,使用16個通道將更為有利。每個通道都代表不同的樂器。如果有能夠獨立播放16種不同樂器的合成器,那麼您就可以編寫用於16個波段的管絃樂曲,而且只使用一條MIDI電纜將MIDI卡與合成器連結起來。

MIDI訊息

儘管Note On和Program Change訊息在任何MIDI執行中都是最重要的訊息,但並不是所有的MIDI都可以執行。表22-2是MIDI規格中定義的MIDI通道訊息表。我在前面提到過,狀態位元組的高位元總是設定著,而狀態位元組後面的資料位元組的高位元都等於0。這意味著狀態位元組的範圍是0x80到0xFF,而資料位元組的範圍是0到0x7F。

MIDI和音樂

90 3C 40

90 3C 00

C0 pp

90 kk vv

C0 pp

9n kk vv

Cn pp

C0 01 C1 05

90 kk vv 91 kk vv

表22-2 MIDI通道訊息(n =通道代號,從0到15)

雖然沒有嚴格的要求,鍵號通常還是與西方音樂的傳統音符相對應(例如,對於打擊聲音,每個鍵號碼可以是不同的打擊樂器)。當鍵號與鋼琴類的鍵盤對應時,鍵60(十進位)是中音C。MIDI鍵號在普通的88鍵鋼琴範圍的基礎上向下擴展了21個音符,向上擴展了19個音符。速度代號是按下某鍵的速度,在鋼琴上它控制聲音的響度與和諧特徵。特殊的聲音可以依這種方式或其他方式來回應鍵的速度。

前面展示的例子使用帶有0速度位元組的Note On訊息來表示Note Off命令。對於鍵盤(或者其他控制器)還有一個單獨的Note Off命令,該命令實作釋放鍵的速度,不過,非常少見。

還有兩個「接觸後」訊息。接觸後是一些鍵盤的特徵,按下某個鍵以後,再用力按下鍵可以在某些方式上改變聲音。一個訊息(狀態位元組0xDn)是將接觸後應用於通道中目前演奏的所有音符,這是最常見的。狀態位元組0xAn表示獨立應用每個單獨鍵的接觸後。

通常,鍵盤上都有一些用於進一步控制聲音的刻度盤或開關。這些裝置稱為「控制器」,所有變化都由狀態位元組0xBn表示。通過從0到121的號碼確認控制器。0xBn狀態位元組也用於Channel Mode訊息,這些訊息顯示了合成器如何在通道中回應同時發生的音符。

一個非常重要的控制器是上下轉換音調的輪,它有一個單獨的MIDI訊息,其狀態位元組是0xEn。

表22-2中所缺少的是狀態位元組以從F0到FF開始的訊息。這些訊息稱為系統訊息,因為它們適用於整個MIDI系統,而不是部分通道。系統訊息通常用於同步的目的、觸發編曲器、重新設定硬體以及獲得資訊。

許多MIDI控制器連續發送狀態位元組0xFE,該位元組稱為Active Sensing訊息。這簡單地表示了MIDI控制器仍依附於系統。

一條重要的系統訊息是以狀態位元組0xF0開始的「系統專用」訊息。此訊息用於將資料塊按廠商與合成器所依靠的格式傳遞給合成器(例如,用這種方法可以將新的聲音定義從電腦傳遞給合成器)。系統專用訊息只是可以包含多於2個資料位元組的唯一訊息。實際上,資料位元組數是變化的,而每個資料位元組的高位都設定為0。狀態位元組0xF7表示系統專用訊息的結尾。

系統專用訊息也用於從合成器轉儲資料(例如,聲音定義)。這些資料都是通過MIDI Out埠來自合成器。如果要用裝置無關的方式對MIDI編寫程式,則應該盡可能避免使用系統專用訊息。但是它們對於定義新的合成器聲音是非常有用的。

MIDI檔案(副檔名是.MDI)是帶有定時資訊的MIDI資訊集,可以用MCI播放MIDI檔案。不過,我將在本章的後面討論低階midiOut函式。

MIDI編曲簡介

低階MIDI的API包括字首為midiIn和midiOut的函式,它們分別用於讀取來自外部控制器的MIDI序列和在內部或外部的合成器上播放音樂。儘管其名稱為「低階」,但使用這些函式時並不需要瞭解MIDI卡上的硬體介面。

要在播放音樂的準備期間打開一個MIDI輸出設備,可以呼叫midiOutOpen函式:

如果呼叫成功,則函式傳回0,否則傳回錯誤代碼。如果參數設定正確,則常見的一種錯誤就是MIDI設備已被其他程式使用。

該函式的第一個參數是指向HMIDIOUT型態變數的指標,它接收後面用於MIDI輸出函式的MIDI輸出代號。第二個參數是設備ID。要使用真實的MIDI設備,這個參數範圍可以是從0到小於由midiOutGetNumDevs傳回的數值。您還可以使用MIDIMAPPER,它在MMSYSTEM.H中定義為-1。大多數情況下,函式的後三個參數設定為NULL或0。

一旦打開一個MIDI輸出設備並獲得了其代號,您就可以向該設備發送MIDI訊息。此時可以呼叫:

第一個參數是從midiOutOpen函式獲得的代號。第二個參數是包裝在32位元DWORD中的1位元組、2位元組或者3位元組的訊息。我在前面討論過,MIDI訊息以狀態位元組開始,後面是0、1或2位元組的資料。在dwMessage中,狀態位元組是最不重要的,第一個資料位元組次之,第二個資料位元組再次之,最重要的位元組是0。

例如,要在MIDI通道5上以0x7F的速度演奏中音C(音符是0x3C),則需要3位元組的Note On訊息:

midiOutShortMsg的參數dwMessage等於0x007F3C95。

三個基礎的MIDI訊息是Program Change(可為某一特定通道而改變樂器聲音)、Note On和Note Off。打開一個MIDI輸出設備後,應該從一條Program Change訊息開始,然後發送相同數量的Note On和Note Off訊息。

當您一直演奏您想演奏的音樂時,您可以重置MIDI輸出設備以確保關閉所有的音符:

然後關閉設備:

使用低階的MIDI輸出API時,midiOutOpen、midiOutShortMsg、midiOutReset和midiOutClose是您需要的四個基礎函式。

現在讓我們演奏一段音樂。BACHTOCC,如程式22-8所示,演奏了J. S. Bach著名的風琴演奏的D小調《Toccata and Fugue》中托卡塔部分的第一小節。

error = midiOutOpen (&hMidiOut, wDeviceID, dwCallBack, dwCallBackData, dwFlags) ;

error = midiOutShortMsg (hMidiOut, dwMessage) ;

0x95 0x3C 0x7F

midiOutReset (hMidiOut) ;

midiOutClose (hMidiOut) ;

程式22-8 BACHTOCC BACHTOCC.C /*----------------------------------------------------------------------------- BACHTOCC.C -- Bach Toccata in D Minor (First Bar) (c) Charles Petzold, 1998 -----------------------------------------------------------------------------*/ #include <windows.h> #define ID_TIMER 1 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 note if (iIndex != sizeof (noteseq) / sizeof (noteseq[0]) && noteseq[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的第一小節。

在這裏要做的就是把音樂轉換成一系列的數值-基本鍵號和定時資訊,其中定時資訊表示發送Note On(對應於風琴鍵按下)和Note Off(釋放鍵)訊息的時間。由於風琴鍵盤對速度不敏感,所以我們用相同的速度來演奏所有的音符。另外一個簡化是忽略斷奏(即,在連續的音符之間留下一個很短的停頓,以達到尖硬的效果)和連奏(在連續的音符之間有更圓潤的重疊)之間的區別。我們假定一個音符結束後面緊接著下一個音符開始。

如果看得懂樂譜,那麼您就會注意到托卡塔曲以兩個平行的八度音階開始。因此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發送一條Program Change訊息來選擇「教堂風琴」的聲音。在General MIDI聲音配置中,教堂風琴聲音在Program Change訊息中用數位位元組19表示。實際演奏的音符出現在WM_TIMER訊息處理期間。用迴圈來處理兩個音符的多音。如果前一個音符還在演奏,BACHTOCC就為該音符發送Note Off訊息。然後,如果下一個音符不是休止符,則向通道0和12發送Note On訊息。隨後,重置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鍵盤來演奏電子音樂合成器-不管是連結在音效卡上的,還是掛接在MIDI Out埠的外部合成器。KBMIDI讓您完全控制MIDI輸出設備(即內部或外部的合成器)、MIDI通道和樂器聲音。除了演奏時的趣味性以外,我還發現此程式對於開發Windows如何實作MIDI支援很有用。

圖22-1 Bach的D小調Toccata and Fugue的第一小節

程式22-9 KBMIDI KBMIDI.C /*-------------------------------------------------------------------------- KBMIDI.C -- Keyboard MIDI Player (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> // Defines for Menu IDs // -------------------- #define IDM_OPEN 0x100 #define IDM_CLOSE 0x101 #define IDM_DEVICE 0x200 #define IDM_CHANNEL 0x300 #define IDM_VOICE 0x400 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName [] = TEXT ("KBMidi") ; HMIDIOUT hMidiOut ; int iDevice = MIDIMAPPER, iChannel = 0, iVoice = 0, iVelocity = 64 ; int cxCaps, cyChar, xOffset, yOffset ; // Structures and data for showing families and instruments on menu // -------------------------------------------------------------------- typedef struct { TCHAR * szInst ; int iVoice ; } 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 5 E 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 5 A 5, 11, 1, 13, TEXT ("B"), // 22 u 5 B 6, 0, 1, 15, TEXT ("C"), // 23 i 6 C 6, 2, 1, 17, TEXT ("D"), // 24 o 6 D 6, 4, 1, 19, TEXT ("E"), // 25 p 6 E 6, 5, 1, 21, TEXT ("F"), // 26 [ 6 F 6, 7, 1, 23, TEXT ("G"), // 27 ] 6 G -1, -1, -1, -1, NULL, // 28 Ent -1, -1, -1, -1, NULL, // 29 Ctrl 3, 8, 2, 2, TEXT ("G#"), // 30 a 3 G# 3, 10, 2, 4, TEXT ("A#"), // 31 s 3 A# -1, -1, 2, 6, TEXT (""), // 32 d 4, 1, 2, 8, TEXT ("C#"), // 33 f 4 C# 4, 3, 2, 10, TEXT ("D#"), // 34 g 4 D# -1, -1, 2, 12, TEXT (""), // 35 h 4, 6, 2, 14, TEXT ("F#"), // 36 j 4 F# 4, 8, 2, 16, TEXT ("G#"), // 37 k 4 G# 4, 10, 2, 18, TEXT ("A#"), // 38 l 4 A# -1, -1, 2, 20, TEXT (""), // 39 ; 5, 1, 2, 22, TEXT ("C#"), // 40 ' 5 C# -1, -1, -1, -1, NULL, // 41 ` -1, -1, -1, -1, NULL, // 42 Shift -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 3 B 4, 0, 3, 7, TEXT ("C"), // 46 c 4 C 4, 2, 3, 9, TEXT ("D"), // 47 v 4 D 4, 4, 3, 11, TEXT ("E"), // 48 b 4 E 4, 5, 3, 13, TEXT ("F"), // 49 n 4 F 4, 7, 3, 15, TEXT ("G"), // 50 m 4 G 4, 9, 3, 17, TEXT ("A"), // 51 , 4 A 4, 11, 3, 19, TEXT ("B"), // 52 . 4 B 5, 0, 3, 21, TEXT ("C") // 53 / 5 C } ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { MSG msg; HWND hwnd ; 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) { TCHAR szBuffer [32] ; HMENU hMenu, 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 off DrawKey (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 ; HMENU hMenu ; int i, iNumDevs, iPitchBend, cxClient, cyClient ; MIDIOUTCAPS moc ; PAINTSTRUCT ps ; SIZE size ; TCHAR szBuffer [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鍵以110 Hz的頻率演奏A。鍵盤的最下行,右邊是中音C,倒數第二行為其升音或降音。上面兩行鍵繼續按此規律變化,從中音C到G#。這樣,整個範圍是三個八度音階。另外,分別按Shift鍵和Ctrl鍵可使整個音域上升或下降1個八度音階,這樣有效的音域就是5個八度音階。

不過,如果立即開始演奏,那麼您將聽不到任何聲音。您必須先從「Status」功能表中選擇「Open」,打開一個MIDI輸出設備。如果埠打開成功,則按下一個鍵就向合成器發送一條MIDI Note On訊息,釋放鍵則產生一條Note Off訊息。取決於鍵盤的按鍵特性,您可以同時演奏幾個音符。

從「Status」功能表裏選擇「Close」來關閉MIDI設備。這對於需要在不終止KBMIDI程式的情況下執行Windows下的其他MIDI軟體來說是很方便的。

「Device」功能表列出了已安裝的MIDI輸出設備,這些設備通過呼叫midiOutGetDevCaps函式獲得。其中有些設備可能是MIDI Out埠連結的實際存在或不存在的外部合成器。列表還包括MIDI Mapper設備。這是從「控制台」的「多媒體」中選擇的MIDI合成器。

「Channel」功能表用來選擇從1到16的MIDI通道,內定狀態下選擇通道1。KBMIDI程式產生的所有MIDI訊息都發送到所選的通道。

KBMIDI最後一個功能表項是「Voice」,它是一個雙層功能表,用於選擇128種樂器聲音,這些聲音在General MIDI規範中定義並在Windows中實作。這128種樂器聲音分為16樂器組,每個樂器組有8種樂器。由於不同的MIDI鍵號對應於不同的泛音,所以這128種樂器聲音也稱為有旋律的聲音。

General MIDI中還定義了大量無旋律的打擊樂器。要演奏打擊樂器,可以從「Channel」功能表選擇通道10,還可以從「Voice」功能表選擇第一種樂器聲音(「Acoustic Grand Piano」)。這樣,按不同的鍵就可以得到不同打擊樂器的聲音。從MIDI鍵號35(低於中音C兩個八度音階的B)到81(高於中音C近兩個八度音階的A),共有47種不同的打擊樂器聲音。在下面的DRUM程式中就利用了打擊樂器通道。

KBMIDI程式有水平和垂直捲動列。由於PC鍵盤對按鍵速度不敏感,所以用水平捲動列來控制音符速度。一般來說,這與演奏音符的音量一致。設定完水平捲動列以後,所有的Note On訊息都將使用這個速度。

垂直捲動列將產生一條稱為「Pitch Bend」的MIDI訊息。要使用此特性,請按下一個或多個鍵,然後用滑鼠拖動捲動列。向上拖動捲動列音符頻率將上升,向下拖動則頻率下降。釋放捲動列後將恢復正常的基音。

這兩個捲動列要小心使用:因為拖動捲動列時,鍵盤訊息將不進入程式的訊息迴圈。因此,如果按下一個鍵後就開始拖動捲動列,然後在完成拖動之前就釋放了該鍵,那麼音符仍將發聲。所以,拖動捲動列時不要按下或者釋放任何鍵。對功能表也有類似的規則:按著鍵時不要進行功能表選擇。另外,在按下與釋放某個鍵期間,不要用Ctrl或Shift鍵來改變八度音階。

如果一個或者多個音符出現「粘滯現象」,即釋放後繼續發聲,那麼請按下Esc鍵。按下此鍵將通過向MIDI合成器的16個通道發送16條All Notes Off訊息,來關閉聲音。

KBMIDI沒有資源描述檔,而是通過搜索來建立的功能表。設備名稱從midiOutGetDevCaps函式獲得,樂器種類和名稱則儲存在程式的一個大資料結構中。

KBMIDI定義了幾個小函式來簡化MIDI訊息。除了Pitch Bend訊息以外,其他訊息都在前面討論過了。Pitch Bend訊息用兩個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」功能表裏選擇。

但是,其他許多打擊樂器都沒有旋律,它們不能調音,而且通常含有太多的噪音,以致不能與某個基音相聯繫。在「General MIDI」規範中,這些沒旋律的打擊樂器聲在通道10有效。不同的鍵號對應47種不同的打擊樂器。

DRUM程式,如程式22-10所示,是一個電腦擊鼓器。此程式讓您用47種不同的打擊樂器的聲音來構造最大到32個音符的一個序列,然後在選擇的速度和音量下反覆演奏這個序列。

程式22-10 DRUM DRUM.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 *) ; int AskAboutSave (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 ; int cxChar, 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) { static BOOL bNeedSave ; static DRUM drum ; static HMENU hMenu ; static int iTempo = 50, iIndexLast ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; HDC hdc ; int i, x, y ; PAINTSTRUCT ps ; POINT point ; 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 parameters Tempo = (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 ; int iIndex ; 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 notes MidiOutMessage (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] ; HMMIO hmmio ; int iFormat = 2 ; MMCKINFO mmckinfo [3] ; SYSTEMTIME st ; 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 DRUM MENU 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 #define IDM_FILE_NEW 40001 #define IDM_FILE_OPEN 40002 #define IDM_FILE_SAVE 40003 #define IDM_FILE_SAVE_AS 40004 #define IDM_APP_EXIT 40005 #define IDM_SEQUENCE_RUNNING 40006 #define IDM_SEQUENCE_STOPPED 40007 #define IDM_APP_ABOUT 40008

當第一次執行DRUM時,您將看到在視窗中有兩列,左邊一列按名稱列出了47種不同的打擊樂器。右邊的網格是打擊樂器的聲音與時間的二維陣列。每一個打擊器都對應網格中的一列。32行就是32拍。如果要讓這32拍出現在一個4/4拍的小節中(即每小節4個四分音符),那麼每1拍對應一個三十二分音符。

從「Sequence」功能表選擇「Running」時,程式將試圖打開MIDI Mapper設備。如果失敗,螢幕將出現一個訊息方塊。否則,您將看到一個「跳動的小球」隨演奏的節拍在網格底部跳過。

在網格的任何位置單擊滑鼠左鍵可以在此拍中演奏打擊樂器的聲音,這時區域將變成暗灰色。用滑鼠右鍵還可以添加鋼琴的拍子,這時區域將會變成亮灰色。如果按下兩個鍵(同時或分別),此區域將變成黑色,而且可以同時聽到打擊樂器和鋼琴的聲音。再次單擊其中的一個鍵或雙鍵將關閉該拍中的聲音。

網格上部是每4拍一個點。這些點使我們不用過多的計算就可以很簡易地確定單擊的位置。網格的右上角是一個冒號和一條豎線(:|),它們看起來像傳統音樂符號中的反覆記號。這個符號表示序列的長度。您可以通過單擊滑鼠來將反覆記號放置於網格內的任意位置。該序列最多(但不包括)只能演奏反覆記號以內的拍子。如果要建立華爾茲節奏,則應將反覆記號設定為3拍的若干倍。

水平捲動列控制MIDI Note On訊息中的速率位元組。這雖然能改變一些合成器的音質,但一般會影響音量。程式起初將速率捲動列設定在中間位置。豎直捲動列控制拍子。這是對數刻度,範圍從每拍1秒(捲動列在底部)到每拍10毫秒(捲動列在頂部)。程式最初將拍子設定為每拍100毫秒(1/10秒),這時捲動列在中間。

「File」功能表允許您儲存和讀取副檔名為.DRM的檔案,這是我定義的一種格式。這些檔案很小並採用了RIFF的檔案格式,這是一種所有新的多媒體資料檔案推薦使用的格式。「Help」功能表中的「About」選項顯示一個對話方塊,該對話方塊用一段非常簡明的摘要來說明滑鼠在網格中的用法以及兩個捲動列的功能。

最後,「Sequence」功能表中的「Stopped」選項用於目前序列結束後終止樂曲並關閉MIDI Mapper設備。

多媒體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打開MIDI Mapper輸出設備,如果成功,則發送Program Change訊息來為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從為目前演奏的聲音發送MIDI Note Off訊息開始。iIndex的初始值-1以防止第一次啟動序列時發生這種情況。

接下來,iIndex遞增並將其值連同使用者定義的一個WM_USER_NOTIFY訊息一起傳遞給DRUM中的視窗代號。wParam訊息參數設定為iIndex,以便在DRUM.C中,WndProc能夠移動網格底部的「跳動的小球」。

DrumTimerFunc將下列事件作為結束:把Note On訊息發送給通道0和9的合成器上,並儲存網格值以便下一次可以關閉聲音,然後透過呼叫timeSetEvent來設定新的只發生一次的計時器事件。

要停止序列,DRUM呼叫DrumEndSequence,其中唯一的參數可以設定為TRUE或FALSE。如果是TRUE,則DrumEndSequence按下面的程序立即結束序列:刪除所有待決的計時器事件,呼叫timeEndPeriod,向兩個MIDI通道發送「all notes off」訊息,然後關閉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(Resource Interchange File Format:資源交換檔案格式),即一般建議使用的多媒體檔案型態。當然,您可以用標準檔案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(creation data:建立資料)資料塊,並再次使用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年的《PC Magazine》發表。那時,Windows支援兩個不同等級的MIDI合成器(稱為「基本的」和「擴展的」)。那個程式寫的檔案有格式識別字1。 本章的DRUM程式 將格式識別字設定為2。不過,它可以讀取並轉換早期的格式。這在DrumFileRead常式中完成。