摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P1035

乐器数字接口(Musical Instrument Digital Interface, MIDI) 是在 20 世纪 80 年代初期由电子音乐合成器制造商的一个合作组织开发的。MIDI 是各种电子乐器之间及其和计算机之间相互连接的一个协议。它在电子音乐领域是一个极其重要的标准。MIDI 规范由 MIDI 制造商协会(MMA)维护,该协会的网站是http://www.midi.org

22.3.1  MIDI 的工作方式

MIDI 定义了一个通过连接线来传送数字命令的协议。MIDI 连接线使用 5 针 DIN 接口,但是只使用了其中三根针。一根是屏蔽,一根是电流回路,第三根传送数据。MIDI 协议是单向的,每秒传送 31250 位。数据的每个字节有一个起始位和一个结束位,所以有效的传输速率为每秒 3125 字节。

需要了解的很重要的一点是,实际的声音——不管是模拟的还是数字的格式——不会通过 MIDI 连接线传送。连接线传送的一般都是简单的消息,通常为 1 个、2 个或者 3 个字节长。

一种简单的 MIDI 配置包括两个 MIDI 兼容的硬件。其中一个是 MIDI 键盘,它本身不发声,唯一的用途就是产生 MIDI 消息。这个键盘有一个 MIDI 端口,标记为 “MIDI Out” (MIDI 输出)。你可以用一根 MIDI 连接线把这个接口和一个MIDI 声音合成器的 “MIDI In” (MIDI 输入)端口连接起来。这个合成器看起来可能就是一个小盒子,在前面有几个按钮。

当在键盘上按一个键时(假设是中音 C 调的 C 音符),键盘就会传送 3 个字节到 MIDI 输出端口。用十六进制表示,这些字节表示如下:

90 3C 40

第一个字节(90)表示这是一个“音符演奏” (Note On)消息。第二个字节是关键数字,3C 就是表示中音 C 调的 C 音符。第三个字节是击键的力度,范围可以从 1 到 127。我们在这里正好使用的是一个对力度不敏感的键盘,因此它只传送了一个平均力度值。这个 3 字节的消息沿着 MIDI 连接线进入合成器的 MIDI 输入端口。合成器对此的反应就是演奏中音 C 调的 C 音符。

当放开键盘上的键时,键盘又会通过 MIDI 输出端口传送另外一个 3 字节长的消息:

90 3C 00

这个命令和“音符演奏”命令一样,但是力度字节为零。 这个零字节表明这是一个“音符停止” (Note Off)命令,意味音符应该中止。合成器对此的反应就是停止发声。

如果合成器有复音(polyphony)功能(即同时演奏多个音符),则在键盘上可以演奏和弦。键盘会产生多个“音符演奏”消息,合成器会演奏所有的音符。当放开和弦时,键盘则传送多个“音符停止”消息到合成器。

一般来说,在这种配置中的键盘被称为“MIDI 控制器”。它负责产生 MIDI 消息来控制合成器。MIDI 控制器看起来并不一定总像键盘。有的 MIDI 管乐控制器看起来像单簧管或萨克斯管,还有 MIDI 吉他控制器,MIDI 弦乐控制器和 MIDI 鼓乐控制器。所有这些控制器都至少能产生 3 字节的“音符演奏”和“音符停止”消息。

除了看起来像键盘或传统乐器外,控制器也可以是一个“音序器” (sequencer)。这是一种硬件,它能够在内存中存储“音符演奏”和“音符停止”消息序列然后演奏出来。现在,独立的音序器的使用比从前少得多了,因为它们已经被计算机替代了。一台装备了 MIDI 卡的计算机也可以产生“音符演奏”和“音符停止”消息来控制合成器。MIDI 创作软件(可以让你在屏幕上作曲)可以存储来自 MIDI 控制器的 MIDI 消息,让你对它们进行修改,然后再把 MIDI 消息传送给合成器。

合成器有时也称作“发声模块”(sound module)或“音频发生器”(tone generator)。MIDI 不指定声音实际上是怎么生成的。合成器可以使用各种不同的声音生成技术。

在现实中,只有那些非常简单的 MIDI 控制器(例如管乐控制器)才只配有 MIDI 输出端口。通常一个 MIDI 键盘还含有一个内置合成器,并且有标记为 MIDI 输入(MIDI In)、MIDI 输出(MIDI Out)和 MIDI 通过(MIDI Thru)的三个 MIDI 端口。MIDI 输入端口会接收 MIDI 消息,并在键盘内置的合成器上演奏。MIDI 输出端口用来发送键盘产生的 MIDI 消息到外部的合成器。MIDI 通过端口是一个输出端口,它把 MIDI 输入的消息复制到 MIDI 通过端口——所有从 MIDI 输入端口传入的消息都被传送到 MIDI 通过端口进行输出。(MIDI 通过端口不会输出 MIDI 输出端口输出的信息。)

连接 MIDI 硬件只有两种方式:你可以把一个硬件的 MIDI 输入连接到另一个硬件的 MIDI 输出,或者把一个硬件的 MIDI 输入连接到另一个硬件的 MIDI 通过。MIDI 通过端口可以让 MIDI 合成器链式连接起来。

22.3.2  音色变换

合成器能够生成哪些声音呢?是钢琴声、小提琴声、小号声,还是飞碟声呢?通常合成器能够生成的各种声音都存储在 ROM 或者其他某个地方。它们通常被称为“声音” (voice)、“乐器” (instrument) 或者“音色” (patch)。(patch 一词来源于模拟合成器时代,那时通过将连接线(patch cord)插入到合成器前面的插孔来设定不同的声音。)

在 MIDI 中,合成器能够生成的各种声音称为 program(这里我们把该术语翻译成音色)。改变音色需要给合成器发送如下的 MIDI 音色变换 (program change)消息:

C0 pp

其中,pp 的取值范围从 0 到 127.通常 MIDI 键盘顶部有一系列编了号的按钮可以生成音色变换消息。通过按下这些编号按钮,你可以用键盘控制合成器的声音。这些按钮的编号通常从 1 而不是 0 开始,所以音色编号 1 对应音色变换字节 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”。所以,如果在用键盘生成一个音色变换时不想感到意外的话,最好了解一下乐器声和使用合成器的每个音色编号之间的对应关系。

对于包含音色变换消息的 MIDI 文件来说,这是一个实实在在的问题。因为它们的内容在不同的合成器上听起来是不同的,所以这些文件不是设备无关的。但是,近几年一个通用 MIDI(General MIDI,简称 GM)标准把这些音色编号标准化了。Windows 支持通用 MIDI 标准。如果一个合成器和通用 MIDI 规范不一致,可以通过音色映射使它仿真一个通用 MIDI 合成器。

22.3.3  MIDI 通道

目前为止,我们已经讨论了两条 MIDI 消息。第一条是音符演奏(note on)消息,格式如下:

90 kk vv

其中,kk 是键号(0 到 127),vv 是力度(0 到 127)。力度为 0 时表示音符停止(note off)指令。第二条是音色变换(program change)消息,

C0 pp

其中,pp 的取值范围是从 0 到 127。这些是典型的 MIDI 消息。第一个字节称作状态字节。根据状态字节值的不同,其后通常跟随 0、1 或者 2 个字节的数据(我们即将谈到的系统独占消息是个例外)。区分状态字节和数据字节很容易:状态字节的高位总是 1,而数据字节高位总是 0。

然而我们还没有讨论过这两条消息的通用格式。音符演奏消息的通用格式是

9n kk vv

音色变换的通用格式是

Cn pp

在这两种情况下, n 都表示状态字节的低四位,取值范围从 0 到 15,称为 MIDI “通道”。通道通常从 1 开始编号,所以如果 n 是 0,则表示通常 1。

使用 16 个不同的通道使得 MIDI 电缆可传输 16 种不同声音的消息。通常,可以发现典型的 MIDI 消息的字符串都是如下格式:以音色变换消息开始,以便为要使用的各通道设置声音,接下来是多个音符演奏和音符停止命令。再后面可能是其他的音色变换命令。但任何时候,每个通道只和一个声音相关联。

让我们看一个简单的例子:假设我们描述的键盘控制器能够同时为两个不同的通道(通道 1 和通道 2)生成 MIDI 消息。你可以先在键盘上按下按钮,以发送两条音色变换消息到合成器:

C0 01
C1 05

现在我们为音色 2 设定了通道 1,为音色 6 设定了通道 2。(回忆一下之前谈过的,通道编号和音色编号是从 1 开始的,但在消息中编码都是从 0 开始的。)现在当你在键盘上按一个键时,它将发送两个音符演奏消息,每个通道一个:

90 kk vv
91 kk vv

这就允许你同时演奏两种乐器的声音。

另一种用法是“拆分”键盘。低位键可以在通道 1 生成音符演奏消息,高位键可以在通道 2 生成音符演奏消息。这就允许在一个键盘上独立演奏两种乐器。

如果考虑 PC 上的 MIDI 音序软件,使用 16 个通道则更有威力。每个通道代表一个不同的乐器。如果有可以独立演奏 16 种不同乐器的合成器时,就可以为一个 16 件乐器的乐团编曲了,而且只需要用一根 MIDI 连接线连接合成器和 MIDI 卡。

22.3.4  MIDI 消息

虽然音符演奏和音色变换消息是任何 MIDI 实现中最重要的消息,但它们并不是 MIDI 的全部。图 22-9 是 MIDI 规范里定义的 MIDI 通道消息表。正如我前面提到过的那样,状态字节的高位置总是 1,而状态字节后的数据字节的高位则等于 0。这意味着状态字节的取值范围是从 0x80 到 0xFF,而数据字节的取值范围是从 0 到 0x7F。

表 22-9  MIDI 通道消息 ( n 是通道编号,从 0 到 15 )
MIDI 消息 数据字节
 音符停止 (Note Off)  8n kk vv  kk = 键号 (0-127) 
 vv = 力度 (0-127)
 音符演奏 (Note On)  9n kk vv  kk = 键号 (0-127)
 vv = 力度 (1-127,0 = 音符停止)
 复音触后 (Polyphonic After Touch)   An kk tt  kk = 键号 (0-127)
 tt = 触后 (0-127)
 控制变换 (Control Change)  Bn cc xx  cc = 控制器 (0-121)
 xx = 值 (0-127)
 通道模式本地控制 (Channel Mode Local Control)  Bn 7A xx  xx = 0 (关),127 (开)
 所有音符停止 (All Notes Off)  Bn 7B 00  
 全通道模式关 (Omni Mode Off)  Bn 7C 00  
 全通道模式开 (Omni Mode On)  Bn 7D 00  
 单通道模式开 (Mono Mode On)  Bn 7E cc  cc = 通道编号
 复音模式开 (Poly Mode On)  Bn 7F 00  
 音色变换 (Program Change)  Cn pp  pp = 音色 (0-127)
 通道触后 (Channel After Touch)  Dn tt  tt = 触后 (0-127)
 改变音高轮 (Pitch Wheel Change)  En ll hh  ll = 低七位 (0-127)
 hh = 高七位 (0-127)

键编号通常对应西方音乐的传统音符,但并不是必须的。(例如,对一个打击音,每个键编号可能是一个不同的打击乐器。)当键编号对应到钢琴类的键盘时,键 60(十进制)表示中音 C 调的 C 音符。MIDI 键盘在通常的 88 键钢琴的基础上向下扩展了 21 个音符,向上扩展了 19 个音符。力度编号代表按键的力度,在钢琴上反应为声音响度和和声特性。不同的乐器可以以不同的方式响应击键力度。

前面展示的例子中我使用力度 0 的音符演奏消息表示一个音符停止命令。对于键盘(或其他控制器)还可能有一个单独的音符停止命令,那就可以实现一个松开按键的力度,但是,这很少见。

还有两个“触后” (after touch)消息。触后是某些键盘的一个功能,可以在按下一个键后更用力按压这个键来在某种程度上改变声音。有一个消息(状态字节 0xDn)是将触后应用到通道正在演奏的所有音符上,这是最常见的情况。而状态字节 0xAn 表示触后只应用到各个单独的按键。

通常,键盘有一些来进一步控制音符的旋钮或者开关。它们被称为“控制器”,其状态改变由状态字节 0xBn 来表示。控制器通过 0-121 的编号来标识。状态字节 0xBn 也用于通道模式消息(Channel Mode Message),这些消息指明了合成器如何响应在通道中同时产生的音符。

一个很重要的控制器是一个切换音高的转轮,它有一个单独的 MIDI 消息,状态字节为 0xEn。

表 22-9 中没有涉及 F0 到 FF 状态字节开头的消息。这些消息称为系统消息(system message),因为它们适用于整个MIDI 系统而不是某个特定的通道。系统消息通常用来同步、触发音序、重置硬件或获得信息。

许多 MIDI 控制器会连续发送状态字节 0xFE,这称为活动检测消息(active sensing message)。这只表示 MIDI 控制器仍然和系统相连。

一个重要的系统信息是“系统独占”(system exclusive)消息,以状态字节 0xF0 开始。这个消息用来将数据块以厂商和合成器特定的格式传送到合成器。(例如,使用这个方法可以将新的声音定义从计算机传到合成器。)系统独占消息是唯一一种可以包含超过两个数据字节的消息。事实上,它的数据字节的数量是不定的,但每个数据字节的高位都必须设定为 0。状态字节 0xF7 表示一个系统独占消息的结束。

系统独占消息也用来从合成器转储数据(例如,声音定义)。这些数据通过 MIDI 输出端口从合成器输出。如果试图以设备无关的方式对 MIDI 编程,则可能要避免使用系统独占消息。但对于定义新的合成器声音,它们还是相当有价值的。

MIDI 文件(扩展名.MID)是带有时间信息的 MIDI 消息集。可以使用 MCI 播放 MIDI 文件。不过,在本章的剩余部分,我将讨论的是底层的 midiOut 函数。

22.3.5  MIDI 序列简介

底层 MIDI API 包括前缀为 midiIn 的函数,它们用于从外部控制器读取 MIDI 序列:以及前缀为 midiOut 的函数,它们用于在内部合成器或者外部合成器上演奏音乐。虽然名为底层,不过使用这些函数时,你并不需要了解任何 MIDI 卡的硬件接口。

要准备演奏音乐时,可以调用 midiOutOpen 函数打开一个 MIDI 输出设备:

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

如果调用成功则返回 0,否则返回错误代码。 如果设定的函数参数正确,那么错误通常就表明 MIDI 设备正在被另一个程序使用。

该函数的第一个参数是一个指向 HMIDIOUT 类型变量的指针,它用来接收 MIDI 输出句柄,供随后的 MIDI 输出函数使用。第二个参数是设备 ID。要使用一个实际的 MIDI 设备,这个参数的取值范围应为从 0 至 midiOutGetNumDevs 函数的返回值减 1。或者,可以使用 MIDIMAPPER,它在 MMSYSTEM.H 中被定义为 -1。大多数情况下,可以设定 midiOutOpen 函数的后三个参数为 NULL 或者 0。

一旦打开了一个 MIDI 输出设备,并获得了其句柄,就可以开始发送 MIDI 消息到这个设备了。为此你可以调用下面的函数:

error = midiOutShortMsg (hMidiOut, dwMessage);

第一个参数是从 midiOutOpen 函数获得的句柄。第二个参数是打包成一个 32 位 DWORD 的一个 1 字节、2 字节或者 3 字节的 MIDI 消息。像我们之前讨论过的一样,MIDI 消息以状态字节开始,然后是 0、1 或者 2 字节的数据。在 dwMessage 中,状态字节是最低位,第一个数据字节次之,第二个数据字节再次之。dwMessage 的最高字节是 0。

例如,要在 MIDI 通道 5 上以 0x7F 力度演奏中音 C(音符是 0x3C),你需要一个 3 个字节的音符演奏消息:

0x95 0x3C 0x7F

那么 midiOutShortMsg 函数的 dwMessage 参数就是 0x007F3C95。

三个基本的 MIDI 消息是音色变换(为某一特定通道改变乐器声音)、音符演奏和音符停止。在打开一个 MIDI 输出设备后,应该从一条音色变换消息开始,且发送相同数量的音符演奏和音符停止消息。

当演奏完想演奏的音乐后,可以重置 MIDI 输出设备以确保关闭所有的音符:

midiOutReset (hMidiOut);

然后可以关闭该设备:

midiOutClose (hMidiOut);

midiOutOpen、midiOutShortMsg、midiOutReset 和 midiOutClose 函数是底层 MIDI 输出 API 中最基本的四个函数。

现在,让我们来演奏一段音乐吧!图 22-10 所示的 BACHTOCC 程序演奏了 J. S.巴赫 著名的管风琴曲《d 小调托卡塔和赋格》(Toccata and Fugue in Minor)的托卡塔曲(toccata)部分的第一小节。

/*--------------------------------------------------------BACHTOCC.C -- Bach Toccata in D Minor (First Bar)(c) Charles Petzold, 1998
--------------------------------------------------------*/#include <Windows.h>#define ID_TIMER 1LRESULT 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 dwMessasge = iStatus | iChannel | (iData1 << 8) | (iData2 << 16);return midiOutShortMsg(hMidi, dwMessasge);
}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 deviceif (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);
<pre name="code" class="cpp">                MidiOutMessage(hMidiOut, 0xC0, 12, 19, 0);

SetTimer(hwnd, ID_TIMER, 1000, NULL);return 0;case WM_TIMER:// Loop for 2-note polyphonyfor (i = 0; i < 2; i++){// Note Off messages for previous noteif (iIndex != 0 && noteseq[iIndex - 1].iNote[i] != -1){MidiOutMessage(hMidiOut, 0x80, 0, noteseq[iIndex - 1].iNote[i], 0);MidiOutMessage(hMidiOut, 0x80, 12, noteseq[iIndex - 1].iNote[i], 0);}// Note On messages for new noteif (iIndex != sizeof(noteseq) / sizeof(noteseq[0]) &&noteseq[iIndex].iNote[i] != -1){MidiOutMessage(hMidiOut, 0x90, 0, noteseq[iIndex - 1].iNote[i], 127);MidiOutMessage(hMidiOut, 0x90, 12, noteseq[iIndex - 1].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-11 显示了巴赫的 D 小调托卡塔曲的第一小节。

这里,我们的工作是把这段音乐转换成一系列数字——基本上包括键编号和时间信息,时间信息表示什么时候发送音符演奏消息(等同于按下一个风琴键)和音符停止消息(释放一个键)。因为风琴键盘对力度不敏感,所以我们可以用相同的力度演奏所有的音符。另一个简化是忽略了断奏(staccato)(即在连续的音符间暂停一下以达到更清脆明亮的效果)和连奏(legato)(在连续的音符间的更平滑的重叠混音)之间的不同。这里我们假设一个音符结束后紧接着开始下一个音符。

图 22-11  巴赫的 《D 小调托卡塔和赋格》的第一小节

如果认识五线谱的话,会注意到托卡塔曲以两个平行的八度音度开始。所以我在 BACHTOCC 里创建了一个名为 noteseq 的数据结构来储存一系列的音长及两个键编号。不幸的是,从第二小节开始要求一个更通用的方法来存储这些信息。我将四分音符的音长定义为 1760 毫秒,这意味着八分音符(在音符或者休止符上有一个符尾)将持续 880 毫秒。十六分音符(两个符尾)是 440 毫秒,三十二分音符(三个符尾)是 220 毫秒,六十四分音符(四个符尾)是 110 毫秒。

在第一小节中有两个波音(mordent)——第一个在首音符处,另一个在小节中间。波音用带有短竖线的曲线表示。在巴洛克音乐里,波音符号表示这个音符实际应演奏为三个音符:当前的音符,然后是比当前音符低一个调的音符,然后再是当前的音符。头两个音符应该很快地演奏,在剩余时间内保持第三个音符。例如,第一个音符是一个带波音的 A,就应演奏为 A、G、A。我将前两个音符定义为六十四分音符,因此每个音符是 110 毫秒。

在第一小节里面还有四个延长符号,它们用中间带圆点的半圆形表示。延长符号表示该音符应比标记的时间保持得更长,通常由演奏者决定。对于延长符号,我将延长 50% 的时间。

可以看到,即使转换一段像 D 小调托卡塔开场曲这样简单直白的音乐,也不总是如音乐本身那样简单。

对小节里的每一个平行的音符和休止符,noteseq 结构数组包括了三个数字。音符持续时间后面跟随平行八度音的两个 MIDI 键编号。例如,第一个音符是音长为 110 毫秒的 A。因为中音 C 调的 MIDI 键编号是 60,所以在中音 C 调上面的 A 调的键编号是 69,而高一个八度音阶的 A 编号是 81。因此,noteseq 数组的前三个数值为 110、69 和 81。我使用音符值 -1 来表示休止符。

在 WM_CREATE 消息处理中,BACHTOCC 设置了一个 1000 毫秒的 Windows 计时器,意味着音乐将从第 1 秒开始演奏,然后使用 MIDIMAPPER 设置 ID 调用 midiOutOpen。

BACHTOCC 只要求一种乐器的声音(风琴),所以只需要使用一个通道。为了简化 MIDI 消息的发送,在 BACHTOCC 里我定义了一个小函数 MidiOutMessage。这个函数接收一个 MIDI 输出句柄、一个状态字节、一个通道编号和两字节的数据。它将这些数据打包到一条 32 位的消息里,然后调用 midiOutShortMsg。

在 WM_CREATE 处理的结尾处,BACHTOCC 发送了一条音色变换消息来选择“教堂风琴”声音。根据通用 MIDI 音色表,该程序在音色变换消息中用数据字节 19 来表示教堂风琴的声音。实际演奏的音符出现在 WM_TIMER 消息处理期间。程序使用循环来处理两个音符的复音。如果之前的音符还在演奏,BACHTOCC 会为这个音符发送音符停止消息。接下来,如果新的音符不是休止符,则发送音符演奏消息到通道 0 和 12。然后程序重置 Windows 计时器为 noteseq 结构中指定的音符长度。

音乐结束后,BACHTOCC 销毁这个窗口。在 WM_DESTROY 消息处理期间,程序调用 midiOutReset 和 midiOutClose 函数,然后终止程序。

虽然 BACHTOCC 可以工作而且效果听起来还不错(虽然和真人演奏的风琴不完全相同),但通常情况下,像这样使用 Windows 计时器演奏音乐并不可行。问题在于 Windows 计时器是基于 PC 系统时钟的,它的分辨率并不能满足音乐的要求。更槽糕的是,Windows 计时器不是异步的。如果另一个程序忙时,得到的 WM_TIMER 消息可能有轻微的延迟。如果程序不能立即处理它们,这些 WM_TIMER 消息甚至可能被抛弃。这将使听到的声音非常槽糕。

所以,BACHTOCC 仅用于展示如何调用底层 MIDI 输出函数,但对于精确音乐的演奏使用 Windows 计时器显然不够。这就是为什么 Windows 还提供了一个附加的计时器函数集,当使用底层 MIDI  输出函数时你可以使用它们。这些函数的前缀是 time,可以使用它们来设定分辨率可达 1 毫秒的计时器。在本章末的 DRUM 程序里我将展示如何使用这些函数。

22.3.6  通过 PC 键盘演奏 MIDI 合成器

因为大多数 PC 用户可能没有可连接到他们机器上的 MIDI 键盘,所以我建议用大家都有的带有字母和数字的 PC 键盘来代替。图 22-12 中的名为 KBMIDI 的程序展示了如何使用 PC 键盘来演奏电子音乐合成器——它或者是在声卡里的合成器或者是连接到 MIDI 输出端口的外部合成器。KBMIDI 帮助你完全控制 MIDI 输出设备(即内部或者外部合成器)、MIDI 通道和乐器声音。除了程序演奏时很有趣外,我还发现这个程序对于探索 Windows 如何实现对 MIDI 的支持也很有用处。

/*--------------------------------------------------------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   0x400LRESULT 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 fro 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     15, 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     45, 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     86, 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    Tab5, 0, 1, 1, TEXT("C"),     //  16     q    5    C5, 2, 1, 3, TEXT("D"),     //  17     w    5    D5, 4, 1, 5, TEXT("E"),     //  18     e    5    E5, 5, 1, 7, TEXT("F"),     //  19     r    5    F5, 7, 1, 9, TEXT("G"),     //  20     t    5    G5, 9, 1, 11, TEXT("A"),    //  21     y    5    A5, 11, 1, 13, TEXT("B"),   //  22     u    5    B6, 0, 1, 15, TEXT("C"),    //  23     i    6    C6, 2, 1, 17, TEXT("D"),    //  24     o    6    D6, 4, 1, 19, TEXT("E"),    //  25     p    6    E6, 5, 1, 21, TEXT("F"),    //  26     [    6    F6, 7, 1, 23, TEXT("G"),    //  27     ]    6    G-1, -1, -1, -1, NULL,      //  28    Ent-1, -1, -1, -1, NULL,      //  29    Ctrl3, 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     d4, 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     h4, 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    A3, 11, 3, 5, TEXT("B"),    //  45     x    3    B4, 0, 3, 7, TEXT("C"),     //  46     c    4    C4, 2, 3, 9, TEXT("D"),     //  47     v    4    D4, 4, 3, 11, TEXT("E"),    //  48     b    4    E4, 5, 3, 13, TEXT("F"),    //  49     n    4    F4, 7, 3, 15, TEXT("G"),    //  50     m    4    G4, 9, 3, 17, TEXT("A"),    //  51     ,    4    A4, 11, 3, 19, TEXT("B"),   //  52     .    4    B5, 0, 3, 21, TEXT("C")     //  53     /    5    C
};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("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 menuhMenuPopup = 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 menuhMenuPopup = CreateMenu();// Put MIDI Mapper on menu if it's installedif (!midiOutGetDevCaps(MIDIMAPPER, &moc, sizeof(moc)))AppendMenu(hMenuPopup, MF_STRING, IDM_DEVICE + (int)MIDIMAPPER, moc.szPname);elseiDevice = 0;// Add the rest of the MIDI devicesfor (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 menuhMenuPopup = 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 menuhMenuPopup = 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 53return;if ((iOctave = key[iScanCode].iOctave) == -1) // Non-music keyreturn;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){MidiNoteOff(hMidiOut, iChannel, iOctave, iNote, 0);    // Note offDrawKey(hdc, iScanCode, FALSE);return;}if (0x40000000 & lParam)          // ignore typematicsreturn;MidiNoteOn(hMidiOut, iChannel, iOctave, iNote, iVelocity);   // Note onDrawKey(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 fonthdc = GetDC(hwnd);GetTextExtentPoint(hdc, TEXT("M"), 1, &size);cxCaps = size.cx;cyChar = size.cy;ReleaseDC(hwnd, hdc);// Initialize "Volume" scroll barSetScrollRange(hwnd, SB_HORZ, 1, 127, FALSE);SetScrollPos(hwnd, SB_HORZ, iVelocity, TRUE);// Initialize "Pitch Bend" scroll barSetScrollRange(hwnd, SB_VERT, 0, 16383, FALSE);SetScrollPos(hwnd, SB_VERT, 8192, TRUE);// Get number of MIDI output devices and set up menuif (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 commandif (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 commandelse if (LOWORD(wParam) == IDM_CLOSE && bOpened){CheckMenuItem(hMenu, IDM_OPEN, MF_UNCHECKED);CheckMenuItem(hMenu, IDM_CLOSE, MF_CHECKED);// Turn all keys off and close devicefor (i = 0; i < 16; i++)MidiOutMessage(hMidiOut, 0xB0, i, 123, 0);midiOutClose(hMidiOut);bOpened = FALSE;}// Change MIDI "Device" menu commandelse 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 deviceif (bOpened){SendMessage(hwnd, WM_COMMAND, IDM_CLOSE, 0L);SendMessage(hwnd, WM_COMMAND, IDM_OPEN, 0L);}}// Change MIDI "Channel" menu commandelse 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 commandelse 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 messagecase 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 repaintcase 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: Velocitycase 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 Bendcase 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, bOpened ? 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# 的音阶。因此,整个音域范围是三个八度音阶。按下 Ctrl 键使整个音符下降一个八度音阶,按下 Shift 键上升一个八度音阶,因此,有效范围是五个八度音阶。

如果此时尝试演奏的话,将什么也听不到。你首先必须从 Status 菜单中选择 Open 选项。该操作将接通一个 MIDI 输出设备。如果成功打开端口,那么按下一个键将向合成器发送一个 MIDI 音符演奏消息;松开这个键则生成一个音符停止消息。根据键盘按键冲突处理的不同,可能可以同时演奏几个音符。

从 Status 菜单选项 Close 将关闭 MIDI 设备。这个功能十分有用,特别是希望在 Windows 下运行其他 MIDI 软件而不希望终止 KBMIDI 程序时。

Device 菜单列出了所有已安装的 MIDI 输出设备,该列表是通过调用 midiOutGetDevCaps 函数来获得的。其中有一个是 MIDI 输出端口,但该端口不一定已被连接到外部合成器上。该列表还包括 MIDI 映射(MIDI Mapper)设备。这是在控制面板的多媒体选项中选择的 MIDI 合成器。

Channel 菜单允许你在 1~16 的范围内选择 MIDI 通道。默认选择的是通道 1。KBMIDI 程序生成的所有 MIDI 消息都被发送到选择的通道。

KBMIDI 最后一个菜单为 Voice,它是一个两层的嵌套菜单,从这里可以选择在通用 MIDI 规范里定义并在 Windows 中实现的 128 种乐器声音之一。这 128 种乐器声音分成 16 个乐器族,每个乐器族,每个乐器族包括了 8 种乐器。因为不同的 MIDI 键编号对应不同的音色,所以这 128 个乐器声音称为旋律音。

通用 MIDI 也定义了许多无旋律的打击乐器。如果要演奏这些打击乐器,可以从 Channel 菜单中选择通道 10。同时从 Voice 菜单中选择第一种乐器声音(Acoustic Grand Piano,原声大钢琴)。此后,每个键都会演奏一种不同的打击乐器的声音。一共有 47 种不同的打击乐器声音,从 MIDI 键编号 35(低于中音 C 两个八度音阶的 B)到 81 (高于中音 C 近两个八度音阶的 A)。在后面的 DRUM 程序中,我们将使用打击乐器通道。

KBMIDI 程序带有水平和垂直滚动条。因为 PC 键盘对力度不敏感,所以使用水平滚动条来控制音符力度。通常来说,滚动条的位置与演奏音符的音量一致。设定水平滚动条后,所有的音符演奏消息都使用这个力度。

垂直滚动条可以生成称为弯音(pith bend)的 MIDI 消息。要使用这个功能,需按下一个或多个键,然后用鼠标拖动垂直滚动条。当向上拖动滚动条时,音符频率将增加,向下拖动时,则评论降低。释放滚动条则音高恢复正常。

使用这两个滚动条时需要注意技巧:因为在操作滚动条时,键盘消息将不进入程序的消息循环。所以,在按下一个键后,开始用鼠标拖动滚动条,即使在拖动结束前松开这个键,音符还将继续发声。因此,在拖动滚动条的过程中不要按下或松开任何键。对菜单也应遵守类似的规则:按着键时,不要进行菜单选择。另外,在按下和松开某个键之间,也不要使用 Ctrl 或 Shift 键来改变八度音阶。

如果一个或多个音符“卡住”了,就是说松开后还在继续发声,则请按下 Esc 键。这将发送 16 条“所有音符停止”(all notes off)消息到 MIDI 合成器的所有 16 个通道来停止发声。

KBMIDI 没有使用资源脚本,它从零开始创建自己的菜单。从 midiOutGetDevCaps 函数获得设备名,乐器声音族和名称则存储在程序的一个大数据结构中。

KBMIDI 实现了几个小函数来简化 MIDI 消息。我们之前已经讨论过除了弯音以外的消息了。弯音消息使用两个 7 位值构成了一个 14 位的弯音等级。0 到 0x1FFFF 间的值表示音高降低,0x2001 到 0x3FFF 的值表示音高升高。

当从 Status 菜单选择 Open 时,KBMIDI 为选中的设备调用 midiOutOpen 函数,如果调用成功的话,则继续调用 MidiSetPatch 函数。当改变设备时,如果需要,KBMIDI 必须关闭前一个设备,然后再打开新设备。当改变 MIDI 设备、MIDI 通道或乐器声音时,KBMIDI 还必须调用 MidiSetPath 函数。

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 将把滚动条位置设置在中间,并以参数值 8192 调用 MidiPitchBend 函数。

22.3.7  MIDI 打鼓机器

有些打击乐器,例如木琴或定音鼓,因为可以用不同的音高演奏,是“有旋律的”或“半音阶的” (chromatic)。木琴的木板对应不同的音高,而定音鼓可以调音。可以在 KBMIDI 的 Voice 菜单里选择这两种乐器或一些其他有旋律的打击乐器。

还有许多其他的打击乐器是无旋律的。它们无法调音,且通常包含太多的噪声以至于不能和某一特定的音高相联系。在通用 MIDI 规范里,这些没有旋律的打击乐器声音可在通道 10 得到。我们使用不同的键编号对应 47 种不同的打击乐器。

图 22-13 所示的 DRUM 程序是一个计算机打鼓机器。这个程序允许你使用 47 种不同的打击乐器的声音来构造最多 32 个音符的乐句。该程序会使用选定的节奏和音量反复演奏这个乐句。

/*--------------------------------------------------------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 structuredrum.iMsecPerBeat = 100;drum.iVelocity = 64;drum.iNumBeats = 32;DrumSetParams(&drum);// Other initializationcxChar = LOWORD(GetDialogBaseUnits());cyChar = HIWORD(GetDialogBaseUnits());GetWindowRect(hwnd, &rect);MoveWindow(hwnd, rect.left, rect.top, 77 * cxChar, 30 * cyChar, FALSE);hMenu = GetMenu(hwnd);// Initialize "Volume" scroll barSetScrollRange(hwnd, SB_HORZ, 1, 127, FALSE);SetScrollPos(hwnd, SB_HORZ, drum.iVelocity, TRUE);// Initialize "Tempo" scroll barSetScrollRange(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 patternfor (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 fileif (bNeedSave && IDCANCEL == AskAboutSave(hwnd, szTitleName))return 0;// Open the selected fileif (DrumFileOpenDlg(hwnd, szFileName, szTitleName)){szError = DrumFileRead(&drum, szFileName);if (szError != NULL){ErrorMessage(hwnd, szError, szTitleName);szTitleName[0] = '\0';}else{// Set new parametersiTempo = (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 fileif ((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';}elsebNeedSave = FALSE;DoCaption(hwnd, szTitleName);}return 0;case IDM_APP_EXIT:SendMessage(hwnd, WM_SYSCOMMAND, SC_CLOSE, 0L);return 0;case IDM_SEQUENCE_RUNNING:// Begin sequenceif (!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 sequenceDrumEndSequence(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 coordinatesx = LOWORD(lParam) / cxChar - 40;y = 2 * HIWORD(lParam) / cyChar - 2;// Set a new number of beats of sequenceif (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 beatif (x >= 0 && x < 32 && y >= 0 && y < NUM_PERC){if (message == WM_LBUTTONDOWN)drum.dwSeqPerc[y] ^= (1 << x);elsedrum.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 velocityswitch (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 temposwitch (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 linesfor (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 marksfor (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 throughcase 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;elseiBrush = 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 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 -- 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   5void 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 notificationDrumEndSequence(TRUE);    // Stop current sequence if running// Open the MIDI Mapper output portif (midiOutOpen(&hMidiOut, MIDIMAPPER, 0, 0, 0))return FALSE;// Send Program Change messages for channels 9 and 0MidiOutMessage(hMidiOut, 0xC0, 9, 0, 0);MidiOutMessage(hMidiOut, 0xC0, 0, 0, 0);// Begin sequence by setting a timer eventtimeGetDevCaps(&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 timerif (uTimerID)timeKillEvent(uTimerID);timeEndPeriod(uTimerRes);// turn off all notesMidiOutMessage(hMidiOut, 0xB0, 9, 123, 0);MidiOutMessage(hMidiOut, 0xB0, 0, 123, 0);// close the MIDI portmidiOutClose(hMidiOut);bSequenceGoing = FALSE;}}elsebEndSequence = 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 balliIndex = (iIndex + 1) % drum.iNumBeats;PostMessage(hwndNotify, WM_USER_NOTIFY, iIndex, timeGetTime());// Check if ending the sequenceif (bEndSequence && iIndex == 0){PostMessage(hwndNotify, WM_USER_FINISHED, 0, 0L);return;}// Note On messages for channels 9 and 0for (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 eventuTimerID = timeSetEvent(max((int)uTimerRes, drum.iMsecPerBeat),uTimerRes, DrumTimerFunc, 0, TIME_ONESHOT);if (uTimerID == 0){PostMessage(hwndNotify, WM_USER_ERROR, 0, 0);}
}
/*-------------------------------------------------------------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 -- 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 fro writingif ((hmmio = mmioOpen(szFileName, NULL,MMIO_CREATE | MMIO_WRITE | MMIO_ALLOCBUF)) == NULL)return szErrorNoCreate;// Create a "RIFF" chunk with a "CPDR" typemmckinfo[0].fccType = mmioStringToFOURCC(szDrumID, 0);wError |= mmioCreateChunk(hmmio, &mmckinfo[0], MMIO_CREATERIFF);// Create "LIST" sub-chunk with an "INFO" typemmckinfo[1].fccType = mmioStringToFOURCC(szInfoID, 0);wError |= mmioCreateChunk(hmmio, &mmckinfo[1], MMIO_CREATELIST);// Create "ISFT" sub-sub-chunkmmckinfo[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 stringGetLocalTime(&st);wsprintfA(szDateBuf, "%04d-%02d-%02d", st.wYear, st.wMonth, st.wDay);// Create "ISCD" sub-sub-chunkmmckinfo[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-chunkmmckinfo[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-chunkmmckinfo[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 returnwError |= 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 fileif ((hmmio = mmioOpen(szFileName, NULL, MMIO_READ)) == NULL)return szErrorNotFound;// Locate a "RIFF" chunk with a "DRUM" from-typemmckinfo[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-chunkmmckinfo[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-chunkmmioAscend(hmmio, &mmckinfo[1], 0);// Locate, read, and verify the "data" sub-chunkmmckinfo[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 filemmioClose(hmmio, 0);// Convert format 1 to format 2 and copy the DRUM structure dataif (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 (excerpts)// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"/
//
// Menu
//DRUM MENU DISCARDABLE
BEGINPOPUP "&File"BEGINMENUITEM "&New", IDM_FILE_NEWMENUITEM "&Open...", IDM_FILE_OPENMENUITEM "&Save", IDM_FILE_SAVEMENUITEM "Save &As...", IDM_FILE_SAVE_ASMENUITEM SEPARATORMENUITEM "E&xit", IDM_APP_EXITENDPOPUP "&Sequence"BEGINMENUITEM "&Running", IDM_SEQUENCE_RUNNINGMENUITEM "&Stopped", IDM_SEQUENCE_STOPPED, CHECKEDENDPOPUP "&Help"BEGINMENUITEM "&About...", IDM_APP_ABOUTEND
END/
//
// Icon
//// Icon with lowest ID value placed first to ensure application icon
// remains consistent on all systems.
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"
BEGINDEFPUSHBUTTON   "OK", IDOK, 54, 143, 50, 14ICON            "DRUM", IDC_STATIC, 8, 8, 21, 20CTEXT           "DRUM", IDC_STATIC, 34, 12, 90, 8CTEXT           "MIDI Drum Machine", IDC_STATIC, 7, 36, 144, 8CONTROL         "", IDC_STATIC, "Static", SS_BLACKFRAME, 8, 88, 144, 46LTEXT           "Left Button:\t\tDrum sounds", IDC_STATIC, 12, 92, 136, 8LTEXT           "Right Button:\t\tPiano sounds", IDC_STATIC, 12, 102, 136, 8LTEXT           "Horizontal Scroll:\t\tVelocity", IDC_STATIC, 12, 112, 136, 8LTEXT           "Vertical Scroll:\t\tTempo", IDC_STATIC, 12, 122, 136, 8CTEXT           "Copyright (c) Charles Petzold, 1998", IDC_STATIC, 8, 48, 144, 8CTEXT           """Programming Windows,"" 5th Edition", IDC_STATIC, 8, 60, 144, 8
END
RESOURCE.H (excerpts)// Microsoft Visual C++ 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 映射设备。如果失败,将出现一个消息框。否则,将看到一个跳动的小球在网格底部随着节拍跳过。

用鼠标左键单击网格任何位置,可以在当前的节拍中演奏出打击乐器的声音,此时这个格式将变成深灰色。使用鼠标右键可以添加钢琴拍子,此时这个格子将变成浅灰色。如果同时或分别按下鼠标的两个键,这个格子将变成黑色,此时可以同时听到打击乐器和钢琴的声音。再次单机一个键或两个键将关闭此拍的声音。

网格顶部每 4 拍有一个点。这些点使我们不用太多计数就可以方便地确定按钮的单击。在网格的右上角是一个冒号和一条竖线(:|),它们合起来表示传统音乐概念里的反复记号。这指明了需要重复演奏的乐句长度。你可以在网格上方的任意位置单击鼠标,来放置这个反复记号。这样,该乐句只演奏反复记号之前(不包括这一拍)的拍子。例如,如果想创建一个华尔兹节奏,则应将反复记号设为 3 拍的某个倍数。

水平滚动条控制 MIDI 的音符演奏消息中的力度字节。它虽然可改变某些合成器的音色,但一般情况下只影响音量。程序开始时,将力度滚动条的滑块设定在中间的位置。垂直滚动条控制节拍。这里使用对数刻度,范围从每拍 1 秒(滑块在底部)到每拍 10 毫秒(滑块在顶部)。程序开始时将一拍设定为 100 毫秒(1/10 秒),这时滚动条的滑块位于中央位置。

File 菜单允许保存或者读取扩展名为 .DRM(这是我定义的一种格式)的文件。这些文件相当小,使用的是所有新的多媒体数据文件推荐使用的 RIFF 格式。Help 菜单中的 About 选项显示一个对话框,内容为网格中鼠标和两个滚动条功能的用法的简要说明。

最后,Sequence 菜单中的 Stopped 选项将停止音乐,并在当前乐句演奏完成后关闭 MIDI 映射设备。

22.3.8  多媒体 time 函数

你可能注意到在 DRUM.C 里没有调用任何多媒体函数。所有的实际操作都发生在 DRUMTIME 模块里。

虽然普通的 Windows 计时器简单易用,但对于很在意时间的应用程序来说却是一场灾难。就像我们在 BACHTOCC 程序里看到的那样,音乐演奏就是一个很在意时间的应用程序,因此 Windows 计时器是不适用的。为了提供在 PC 上演奏 MIDI 所需要的精确度,多媒体 API 提供了一个由七个前缀为 time 的函数来实现的高分辨率计时器。这些函数中有一个是多余的,DRUMTIME 只展示了其余六个的使用方法。这些计时器函数和一个运行在独立线程中的回调函数一起工作。系统将根据程序指定的计时器延迟时间来调用这个回调函数。

在使用多媒体计时器时,需要指定两个不同的时间,单位都是毫秒。第一个是延迟时间,第二个称为分辨率。你可以把分辨率看成是容错误差。如果你指定延迟为 100 毫秒,分辨率为 10 毫秒,则计时器实际的延迟范围是 90 到 110 毫秒之间。

开始使用这个计时器前,应当调用如下函数获得计时器设备的能力:

timeGetDevCaps (&timecaps, uSize);

第一个参数是一个指向 TIMECAPS 结构类型的指针,第二个参数是该结构的大小。TIMECAPS 结构只有两个字段,wPeriodMin 和 wPeriodMax。它们是计时器设备驱动程序所支持的最小和最大分辨率值。如在调用 timeGetDevCaps 后查看这些值,你会发现 wPeriodMin 是 1 而 wPeriodMax 是 65535,所以这个函数看起来并不重要。不过, 得到这些分辨率值并将它们用于其他的计时器函数调用是一个好主意。

下一步是调用如下函数指明程序要求的计时器的计时器分辨率的最低值:

timeBeginPeriod (uResolution);

这个值应在 TIMECAPS 结构确定的范围之内。这个调用帮助计时器设备驱动程序为可能使用该计时器的多个程序提供最好的支持。任何对 timeBeginPeriod 的调用都必须与其后的 timeEndPeriod(我将在稍后介绍)调用成对出现。

现在可以使用如下语句真正设定一个计时器事件了:

idTimer = timeSetEvent (uDelay, uResolution, CallBackFunc, dwData, uFlag);

如果有错误发生,函数返回的值将是 0。在这个调用之后,Windows 将在 uDelay 毫秒后调用 CallBackFunc 函数,允许的误差由 uResolution 指定。 uResolution 的值必须大于等于传递给 timeBeginPeriod 的分辨率值。dwData 是程序定义的数据,稍后将传递给 CallBackFunc。最后一个参数可能是 TIME_ONESHOT 或者 TIME_PERIODIC,TIME_ONESHOT 表示在 uDelay 毫秒后调用 CallBackFunc 函数,而 TIME_PERIODIC 会在每隔 uDelay 毫秒后都调用一次 CallBackFunc 函数。

要在 CallBackFunc 函数被调用停止单次计时器事件,或者暂停周期性的计时器事件,可以调用如下语句:

timeKillEvent (idTimer)

调用 CallBackFunc 后,就没有必要删除单次计时器事件了。当程序使用完计时器后,需要调用如下语句:

timeEndPeriod (wResolution);

其中的参数与传递给 timeBeginPeriod 的参数相同。

另外还有两个以 time 前缀开始的函数。下面的函数会返回自 Windows 第一次启动以来到现在的系统时间,单位是毫秒:

dwSysTime = timeGetTime();

下面这个函数要求第一个参数是一个指向 MMTIME 结构的指针,第二个参数是该结构的大小:

timeGetSystemTime(&mmtime, uSize);

虽然在其他环境中 MMTIME 结构可以用来获得非毫秒格式的系统时间,但在这里总是以毫秒为单位返回时间。所以,timeGetSystemTime 是多余的。

回调函数可调用的 Windows 函数是有限的。回调函数可调用 PostMessage、四个计时器函数(timeSetEvent、timeKillEvent、timeGetTime 和多余的 timeGetSystemTime)、两个 MIDI 输出函数(midiOutShortMsg 和 midiOutLongMsg) 和 OutputDebugStr 调试函数。

显然,多媒体计时器是专门为演奏 MIDI 音序而设计的,很少用于其他方面。当然,也可以使用 PostMessage 来向窗口过程通知计时器事件,而窗口过程可以做任何它想做的事,但它本身无法像定时器回调函数那样准确地做出响应。

回调函数有五个参数,但我们只使用其中两种:从 timeSetEvent 返回的计时器 ID 号和最初作为参数传送给 timeSetEvent 的 dwData 值。

DRUM.C 模块在许多地方调用了 DRUMTIEM.C 中的 DrumSetParams 函数:在创建 DRUM 窗口时、在用户单击网格或者拖动滚动条时、在程序从磁盘加载 .DRM 文件时,以及在程序清除网格时。DrumSetParams 的唯一参数是一个指向在 DRUMTIME.H 中定义的 DRUM 结构类型的指针。此结构类型存储节拍长度(以毫秒为单位)、力度(通常对应于音量)、乐句中的节拍数以及两组 47 个 32 位整数,这两组整数用于存储打击乐器和钢琴声音所用的网格设定。这些 32 位整数中的每一位都对应乐句中的一拍。DRUM.C 模块在静态内存中维护一个 DRUM 类型的结构,并在调用 DrumSetParams 时使用一个指向该类型的指针。DrumSetParams 函数仅简单地复制了这个结构的内容。

要开始演奏乐句,DRUM 调用 DRUMTIME 中的 DrumBeginSequence 函数。唯一的参数就是窗口句柄,它起到一个通知的作用。DrumBeginSequence 函数打开 MIDI 映射输出设备,如果成功,则发送音色变换消息来为 MIDI 通道 0 和通道 9 选择乐器声音 0。(通道是从 0 开始计数的,所以数字 9 实际指的是 MIDI 通道 10,即打击乐器通道。另一个通道用于钢琴声音。)DrumBeginSequence 接着调用 timeGetDevCaps 函数和之后的 timeBeginPeriod 函数。在 TIME_RES 常数中定义的计时器分辨率是 5 毫秒,但我定义了一个名为 minmax 的宏,它从 timeGetDevCaps 返回的限制范围内计算分辨率。

下一个调用的是 timeSetEvent,参数为节拍长度、计算出来的分辨率、回调函数 DrumTimerFunc 和 TIME_ONESHOT 常数。DRUMTIME 使用单次计时器,而不是周期性计时器,这样节拍会随着乐句的演奏而动态变化。timeSetEvent 调用之后,计时器设备驱动程序将在延迟时间过后调用 DrumTimerFunc 函数。

DrumTimerFunc 回调函数是 DRUMTIME.C 里的一个函数,它实现了大部分的繁重操作。iIndex 变量存储乐句当前的节拍。回调函数一开始发送 MIDI 的音符停止消息来终止目前演奏的声音。iIndex 初始值被设为 -1 以防止乐句刚演奏时发生这种情况。

DrumTimerFunc 结束前,会发送音符演奏消息到通道 0 和 9 的合成器,保存网格值以便下次可以关闭声音,然后通过调用 timeSetEvent 来设定一个新的单次计时器事件。

DRUM 调用 DrumEndSequence 函数来停止演奏乐句,其唯一的参数可设为 TRUE 或 FALSE。如果是 TRUE,DrumEndSequence 函数会通过以下步骤立刻结束演奏乐句:删除所有等待的计时器事件,调用 timeEndPeriod 函数,发送“所有音符停止”消息到两个 MIDI 通道,然后关闭 MIDI 输出端口。当用户决定终止这个程序时,DRUM 用 TRUE 参数调用 DrumEndSequence。

但是,当用户从 DRUM 里的 Sequence 菜单中选择 Stop 选项时,程序将用 FALSE 参数调用 DrumEndSequence。这允许在结束前演奏完当前的乐句。DrumEndSequence 通过把全局变量 bEndSequence 设置为 TRUE 来响应此调用。如果 bEndSequence 是 TRUE,且拍子索引值已经被设为 0,则 DrumTimerFunc 将向 WndProc 发送用户定义的 WM_USER_FINISHED 消息。WndProc 必须通过以 TRUE 为参数调用 DrumEndSequence 来响应该消息,以便正确关闭定时器和 MIDI 端口。

22.3.9  RIFF 文件 I/O

DRUM 程序也可以保存和获取包含存储在 DRUM 结构中的信息的文件。这些文件使用的资源交换格式(Resource Interchange File Format, RIFF),该格式适用于多媒体文件类型。你可以使用标准的文件 I/O 函数来读写 RIFF 文件,但更简单的方法是使用前缀为 mmio(multimedia input/output)的函数。

在讨论 .WAV 格式时,我们知道 RIFF 是一种标记文件格式,这意味着文件中的数据是以不同长度的数据块来组织的,每个数据块都用一个标记来标识。标记是一个 4 字节的 ASCII 串。这使得标记名称和 32 位整数之间的比较相对容易些。标记之后是数据块的长度和数据。因为文件中的信息并不位于相对于文件开头固定的偏移位置,而是用标记来标识,所以标记文件格式应用范围很广。你可以通过增加额外的标记来增强这种文件格式。当读文件时,程序可以很容易地找到它们需要的数据,并跳过他们不需要或者不理解的标记。

Windows 中的 RIFF 文件由独立的数据块(文件中的信息块)组成。数据块由数据块类型、数据块大小和数据组成。数据块类型是一个 4 字符的 ASCII 标记,4 个字符的中间不能有空格,但结尾可以用空格填充。数据块大小是一个 4 字节(32 位)的值,表示数据块数据的长度。数据块数据必须占用偶数个字节,如需要可以在结尾补 0。因此,数据块的每部分从文件头开始就是用字对齐的(word-aligned)。数据块大小不包括数据块类型和数据块大小字段所占用的 8 个字节,也不包括填充的数据。

对于一些数据块类型,数据块大小字段的值总是相同的,与具体文件无关。当数据库数据是包含信息的固定长度的结构时,就是这种情况。其他的情况下,数据块大小字段的值根据文件而变化。

有两个特殊的数据块类型,分别称为 RIFF 数据块和 LIST 数据块。在一个 RIFF 数据块中,数据以一个 4 个 ASCII 字符表示的结构类型(form type)开始,然后跟着一个或多个子数据块。LIST 数据块与 RIFF 数据块类似,不同之处它是以一个 4 个 ASCII 字符表示的列表类型(list type)开始。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 结构相同。mmioAscend 函数从当前文件指针(现在应位于数据块的结尾)中减去 MMCKINFO 结构的 dwDataOffset 字段的值,并将得到的值存储到数据的前面。mmioAscend 函数还负责在数据块长度不是 2 字节的倍数的情况下对齐数据。

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 的内容写入该数据块,再使用 mmckinfo[2] 来调用 mmioAscend 函数以填充数据块大小字段。这是第一个完整的数据块。下一个数据块也在 LIST 数据块之内。程序接下来继续用 mmckinfo[2]来调用另一个 mmioCreateChunk 以创建一个 ISCD(creation data)数据块。在调用 mmioWrite 来写入数据后,使用 mmckinfo[2] 来调用 mmioAscend 以填充数据块大小字段。这既是这个数据块的结尾,也是 LIST 数据块的结尾。所以,要再次调用 mmioAscend 来填充 LIST 数据块的数据块大小字段,这次要使用最初用来创建 LIST 数据块的 mmckinfo[1]。

mmioCreateChunk 使用mmckinfo[1] 来创建 "fmt " 和 "data" 数据块;当调用 mmioWrite 函数后,同样使用mmckinfo[1] 来调用 mmioAscend  函数。此时,除了 RIFF 数据块本身以外,所以的数据块大小都已填充好了。这时需要再一次使用 mmckinfo[0] 来调用 mmioAscend。最后,调用 mmioClose 函数。

看起来 mmioAscend 调用好像改变了当前文件指针,它确实填充了数据块大小,然而在函数返回时,文件指针会恢复到之前的位置——数据块结束的地方(或可能因对齐数据而增加 1 字节)。从应用程序的角度来看,所有的文件写入都是从头到尾顺序进行的。

在成功调用 mmioOpen 后,除了可能发生磁盘空间不足之外,不会再有其他的问题。我用变量 wError 累计从 mmioCreateChunk、mmioWrite、mmioAscend 和 mmioClose 调用产生的错误代码,上述的每个函数调用都可能因为磁盘空间不足而失败。如果错误发生,则使用 MMIO_DELETE 常量为参数调用 mmioOpen 来删除文件,并向调用者返回错误消息。

读取 RIFF 文件与创建它相似,只是需要调用 mmioRead 而不是 mmioWrite,要调用 mmioDescend 而不是 mmioCreateChunk。下降(descend)一个数据块是指:定位一个数据块并把文件指针移动到数据大小字段之后(对于 RIFF 数据块来说,是定位于结构类型之后;对于 LIST 数据块则是在列表类型之后)。上升(ascend)一个数据块是指:移动文件指针到数据块末尾。mmioDescend 和 mmioAscend 函数都不会把文件指针移动到文件的前面位置。

更早版本的 DRUM 程序在 1992 年的 PC Magazine 上发表。当时,Windows 只支持两个不同层次的 MIDI 合成器(称为基本型和扩展性)。那个程序输出的文件的格式标识符为 1。本章的 DRUM 程序将格式标识符设为 2,但它读取并转化早起的格式。该功能在 DrumFileRead 例程中实现。

22.3 MIDI 和音乐相关推荐

  1. Ubuntu 22.4网易云音乐启动失败处理方法

    Ubuntu 22.04由于更换/升级了一些动态库,系统动态库x86_64-linux-gnu内libgio-2.0.so.0.libpangocairo-1.0.so.0引用库缺少了函数支持, 导致 ...

  2. 电子琴节奏包制作_制作MIDI电子音乐离不开的三件“法宝”

    现在越来越多的年轻人开始对MIDI制作感兴趣,特别是随着电脑的高速发展,原来离普通人很遥远的专业设备可以用软件代替,这无疑是音乐爱好者的福音,体验一下自己当音乐制作人的乐趣吧! 那么什么是MIDI呢? ...

  3. 电子琴节奏包制作_制作MIDI电子音乐需要准备些什么?

    现在越来越多的年轻人开始对MIDI制作感兴趣,特别是随着电脑的高速发展,原来离普通人很遥远的专业设备可以用软件代替,这无疑是音乐爱好者的福音,体验一下自己当音乐制作人的乐趣吧! 那么什么是MIDI呢? ...

  4. 用 Python 播放多声轨 MIDI 文件音乐

    文/世界上的霸主  图片来源于网络 投稿邮箱:pythonpost@163.com 前言: 人人都喜爱音乐,音乐丰富了我们的情感.在历史上,不乏有伟大的作曲家.钢琴家:巴赫.海顿.莫扎特.贝多芬.李斯 ...

  5. 用计算机弹的九八k简谱,作曲 编曲 配器 录音 混音 母带 MIDI 电脑音乐制作(超级全能宝典)...

    超过三度以上的跳进在小节之间是不谐和的.在初学者中,最好不要用. 6 避免过多使用同方向跳进,三度以上的连续跳进会使旋律失去平衡.特别是单旋律的写作时不要这样.通常在超过三度以上的跳进,最好要能反向级 ...

  6. Ubuntu22.04/22.10 网易云音乐打不开的正确解决方案

    网上一堆帖子说什么少了组件.少了包,还要编译安装的,其实根本没有这么麻烦.这里记录一下来自知乎网友@拉布的解答,亲测有效.原帖地址https://zhuanlan.zhihu.com/p/518108 ...

  7. midi是音乐与计算机相结合的产物,2008年江苏省“专转本”计算机应用基础真题与解答.doc...

    2008年江苏省"专转本"计算机应用基础真题与解答 2008年"专转本"计算机应用基础统一考试试题 (考试时间90分钟,满分100分) 一.单项选择题 1. 下 ...

  8. 被MIDI改变的游戏音乐

    虽然MIDI在互联网时代广泛使用,但其实MIDI最早并不是出现在计算机上,更没有出现在游戏音乐中,而是应用在电子乐器上.电子乐器生产厂家为了不同型号的电子乐器的"交流"而产生的,由 ...

  9. 计算机音乐制作专业大纲,《电脑音乐制作》教学大纲[精品].doc

    <电脑音乐制作>教学大纲[精品] <电脑音乐制作>教学大纲 (课程代码 一.课程说明 (一)适用专业: 1.音乐学专业(本科) 2.音乐教育专业(专科) (二)课程类别: 1. ...

  10. WINDOWS程式设计--声音与音乐

    声音与音乐 在Microsoft Windows中,声音.音乐与视讯的综合运用是一个重要的进步.对多媒体的支援起源於1991年所谓的Microsoft Windows多媒体延伸功能(Multimedi ...

最新文章

  1. swoole UDP TCP客户端
  2. ML之FE:对pandas的dataframe中的类别型字段进行数字编码化(类别型特征数值化)并导出映射表daiding
  3. NHibernate Step By Step(2)-继承映射
  4. 安装或卸载英特尔快速存储技术时,提示“此程序有一个挂起的重新启动”
  5. Docker学习总结之docker入门(转自:Vikings翻译)
  6. 决策树留一法python代码_西瓜书 第4章 决策树 读书笔记
  7. C的|、||、、、异或、~、!运算
  8. 数字信号处理实验——语音信号的数字滤波
  9. 软件研发软件基础设施的建设
  10. python安装openpyxl库_Python openpyxl 库
  11. 数据结构与算法-第三章 链表
  12. 学习的爬虫一点小感悟附上爬取淘宝信息的教程
  13. 前台、中台、后台到底是什么?
  14. centos下压缩文件7z解压
  15. UE4第三人称多人联机游戏开发01
  16. 2020书单、影单、电视剧
  17. 强制root工具kingoroot
  18. 这个横行霸道的美食,也是中秋节的一大特色,我用Python爬取京东1546条大闸蟹商品数据带大家看看
  19. IOT(物联网)的七大通信协议
  20. 计算身体质量指数BMI

热门文章

  1. pvs linux_Linux下用于C ++开发的PVS-Studio静态分析器入门
  2. java spider爬虫_一个简单的java网络爬虫(spider)
  3. HackTheBox-Spider WP
  4. 怎么删除微信的手机充值服务器,微信怎么开启和取消自动充值话费功能?
  5. Matlab矩阵和向量中的常用函数
  6. 数据库expecting ''', found 'EOF'异常——原载于我的百度空间
  7. 关于openpyxl读取excel中图片(shape)的注意点
  8. 毕业设计So Easy:基于Java Web学生选课系统
  9. Xcode里的-ObjC,-all_laod和-force_load的作用
  10. 九爷带你玩转 oracle