Welcome

首页 / 软件开发 / .NET编程技术 / UI前沿技术 – WPF应用程序中的MIDI音乐

UI前沿技术 – WPF应用程序中的MIDI音乐2011-07-24 MSDN Charles Petzold每一台 PC 都包含一个内置的 16 人乐队,可播放一些音乐。人们不容易注意此乐队的成员,因为它 们表示的可能是 Windows 所支持的声音和视频功能阵列中利用最不充分的组件。

此 16 人乐队是在符合 MIDI(乐器数字接口)标准的硬件或软件中实现的电子音乐合成器。在 Win32 API 中,以单词 midiOut 开头的函数支持使用 MIDI 合成器播放音乐。

MIDI 支持不是 .NET Framework 的一部分,但如果要在 Windows 窗体或 Windows Presentation Foundation (WPF) 应用程序中访问此 MIDI 合成器,则需要使用 P/Invoke 或外部库。

我在上一期专栏文章中讨论的 CodePlex 上的 NAudio 声音库中提供了 MIDI 支持,发现这一点时, 我感到非常高兴。您可以从 codeplex.com/naudio 下载该库及其源代码。在本文中,我使用的是 NAudio 1.3.8 版。

简单示例

MIDI 可视为波形音频的高级接口,用于处理乐器和乐音。

MIDI 标准形成于 20 世纪 80 年代早期。电子音乐合成器制造商希望通过标准方法来连接电子音乐控 制器(如键盘)和合成器,因此开发出一种系统,通过具有 5 针接头的电缆以 3,125 字节/秒的速度传 输短消息(长度大多为 1、2 或 3 个字节)。

其中两条最重要的消息称为 Note On 和 Note Off。当音乐家按下 MIDI 键盘的某个键时,键盘生成 一条 Note On 消息,指示所按的乐音和键的速度。合成器通过演奏该乐音进行响应,通常键速度越高, 声音越大。音乐家释放键时,键盘生成 Note Off 消息,生成器通过关闭乐音进行响应。没有实际音频数 据通过 MIDI 电缆。

尽管 MIDI 仍用于连接电子音乐硬件,它还是可以完全通过软件在 PC 中使用。声卡可以包含 MIDI 合成器,Windows 本身完全通过软件模拟 MIDI 合成器。

若要在使用 NAudio 库的 WinForms 或 WPF 应用程序中访问该合成器,请将 NAudio.dll 添加为引用 ,并在源代码中包含以下 using 指令:

using NAudio.Midi;

假设应用程序需要演奏单个 1 秒长的乐音,声音类似于钢琴的中央 C。使用以下代码可实现这一功能 :

MidiOut midiOut = new MidiOut(0);
midiOut.Send(MidiMessage.StartNote(60, 127, 0).RawData);
Thread.Sleep(1000);
midiOut.Send(MidiMessage.StopNote(60, 0, 0).RawData);
Thread.Sleep(1000);
midiOut.Close();
midiOut.Dispose();

PC 可能拥有多个 MIDI 合成器的访问权限;MidiOut 构造函数的参数是一个数字 ID,用于选择要打 开的 MIDI 合成器。如果 MIDI 输出设备已被使用,该构造函数将引发异常。

程序可以先使用静态 MidiOut.NumberOfDevices 属性发现存在多少合成器,从而获取有关 MIDI 合成 器的信息。数字 ID 的范围从 0 到设备数减 1。静态 MidiOut.DeviceInfo 方法接受数字 ID,返回一个 描述合成器的 MidiOutCapabilities 类型的对象。(我不准备使用这些功能,在本文后面部分,我只使 用 ID 为 0 的默认 MIDI 合成器。)

MidiOut 类的 Send 方法向 MIDI 合成器发送一条消息。MIDI 消息包含 1、2 或 3 个字节,但 Win32 API(和 Naudio)需要将其打包为一个 32 位整数。MidiMessage.StartNote 和 MidiMessage.StopNote 方法执行此打包操作。Send 的两个参数可分别替换为 0x007F3C90 和 0x00003C80。

StartNote 和 StopNote 的第一个参数是 0 到 127 范围内的代码,用于指示实际乐音,其中值 60 是中央 C。高八度是 72。低八度是 48。第二个参数是按下或释放键的速度。(合成器通常会忽略释放速 度。)这些参数在 0 到 127 范围内。MidiMessage.StartNote 的第二个参数越小,乐音越柔和。(我很 快会讨论第三个参数。)

对 Thread.Sleep 的两次调用会将线程挂起 1,000 毫秒。这是用来确定消息时间的很简单的方法,但 应避免在用户界面线程中使用。为了使乐音在被 Close 调用突然截断之前消失,需要第二次调用 Sleep 。