MIDI

本文向你演示了如何枚举 MIDI(乐器数字接口)设备以及从通用 Windows 应用发送和接收 MIDI 消息。 Windows 10 支持基于 USB(类兼容和大多数专有驱动程序)的 MIDI、基于蓝牙 LE(Windows 10 周年版及更高版本)的 MIDI,以及免费提供的第三方产品中的基于以太网的 MIDI 和路由的 MIDI。

枚举 MIDI 设备

在枚举和使用 MIDI 设备之前,将以下命名空间添加到你的项目。

using Windows.Devices.Enumeration;
using Windows.Devices.Midi;
using System.Threading.Tasks;

ListBox 控件添加到你的 XAML 页面,该控件允许用户选择附加到系统的 MIDI 输入设备之一。 添加另一个控件以列出 MIDI 输出设备。

<ListBox x:Name="midiInPortListBox" SelectionChanged="midiInPortListBox_SelectionChanged"/>
<ListBox x:Name="midiOutPortListBox" SelectionChanged="midiOutPortListBox_SelectionChanged"/>

FindAllAsync 方法的 DeviceInformation 类用于枚举许多不同类型的由 Windows 识别的设备。 若要指明你仅希望使用该方法查找 MIDI 输入设备,请使用 MidiInPort.GetDeviceSelector 返回的选择器字符串。 FindAllAsync 将返回包含通过系统注册的每个 MIDI 输入设备的 DeviceInformationDeviceInformationCollection。 如果返回的集合不包含任何项,则没有可用的 MIDI 输入设备。 如果集合中包含项,循环浏览 DeviceInformation 对象,并将每台设备的名称添加到 MIDI 输入设备 ListBox

private async Task EnumerateMidiInputDevices()
{
    // Find all input MIDI devices
    string midiInputQueryString = MidiInPort.GetDeviceSelector();
    DeviceInformationCollection midiInputDevices = await DeviceInformation.FindAllAsync(midiInputQueryString);

    midiInPortListBox.Items.Clear();

    // Return if no external devices are connected
    if (midiInputDevices.Count == 0)
    {
        this.midiInPortListBox.Items.Add("No MIDI input devices found!");
        this.midiInPortListBox.IsEnabled = false;
        return;
    }

    // Else, add each connected input device to the list
    foreach (DeviceInformation deviceInfo in midiInputDevices)
    {
        this.midiInPortListBox.Items.Add(deviceInfo.Name);
    }
    this.midiInPortListBox.IsEnabled = true;
}

枚举 MIDI 输出设备与枚举输入设备的工作原理完全相同,不同之处在于调用 FindAllAsync 时,你应该指定由 MidiOutPort.GetDeviceSelector 返回的选择器字符串。

private async Task EnumerateMidiOutputDevices()
{

    // Find all output MIDI devices
    string midiOutportQueryString = MidiOutPort.GetDeviceSelector();
    DeviceInformationCollection midiOutputDevices = await DeviceInformation.FindAllAsync(midiOutportQueryString);

    midiOutPortListBox.Items.Clear();

    // Return if no external devices are connected
    if (midiOutputDevices.Count == 0)
    {
        this.midiOutPortListBox.Items.Add("No MIDI output devices found!");
        this.midiOutPortListBox.IsEnabled = false;
        return;
    }

    // Else, add each connected input device to the list
    foreach (DeviceInformation deviceInfo in midiOutputDevices)
    {
        this.midiOutPortListBox.Items.Add(deviceInfo.Name);
    }
    this.midiOutPortListBox.IsEnabled = true;
}

创建设备观察程序帮助程序类

Windows.Devices.Enumeration 命名空间提供了 DeviceWatcher,它可在设备已在系统中添加或删除时,或者设备的信息已更新时通知你的应用。 因为支持 MIDI 的应用通常会关注输入和输出设备,所以此示例将创建可实现 DeviceWatcher 模式的帮助程序类,以便可针对 MIDI 输入和 MIDI 输出设备使用相同的代码,从而避免重复。

将新类添加到你的项目以作为设备观察程序。 在此示例中,该类名为 MyMidiDeviceWatcher。 此部分中的剩余代码将用于实现帮助程序类。

将某些成员变量添加到类中:

  • 监视设备更改的 DeviceWatcher 对象。
  • 包含一个实例的 MIDI 输入端口选择器字符串和另一个实例的 MIDI 输出端口选择器的设备选择器字符串。
  • 使用可用设备的名称填充的 ListBox 控件。
  • 从除 UI 线程之外的某个线程更新 UI 所需的 CoreDispatcher
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
CoreDispatcher coreDispatcher;

添加用于从帮助程序类外部访问当前设备列表的 DeviceInformationCollection 属性。

public DeviceInformationCollection DeviceInformationCollection { get; set; }

在类构造函数中,调用方将传入 MIDI 设备选择器字符串、用于列出设备的 ListBox,以及更新 UI 所需的 Dispatcher

调用 DeviceInformation.CreateWatcher 以创建 DeviceWatcher 类的新实例,从而传入 MIDI 设备选择器字符串。

为观察程序的事件处理程序注册处理程序。

public MyMidiDeviceWatcher(string midiDeviceSelectorString, ListBox midiDeviceListBox, CoreDispatcher dispatcher)
{
    deviceListBox = midiDeviceListBox;
    coreDispatcher = dispatcher;

    deviceSelectorString = midiDeviceSelectorString;

    deviceWatcher = DeviceInformation.CreateWatcher(deviceSelectorString);
    deviceWatcher.Added += DeviceWatcher_Added;
    deviceWatcher.Removed += DeviceWatcher_Removed;
    deviceWatcher.Updated += DeviceWatcher_Updated;
    deviceWatcher.EnumerationCompleted += DeviceWatcher_EnumerationCompleted;
}

DeviceWatcher 具有以下事件:

  • Added - 在新设备添加到系统中时引发。
  • Removed - 在从系统中删除设备时引发。
  • Updated - 在更新与现有设备关联的信息时引发。
  • EnumerationCompleted - 在观察程序完成其所请求的设备类型枚举时引发。

在以上每个事件的事件处理程序中,将调用帮助程序方法 UpdateDevices,以通过当前设备列表更新 ListBox。 因为未在 UI 线程上调用 UpdateDevices 更新 UI 元素和这些事件处理程序,因此每个调用都必须打包在对 RunAsync 的调用中,这将导致指定代码在 UI 线程上运行。

private async void DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
{
    await coreDispatcher.RunAsync(CoreDispatcherPriority.High, () =>
    {
        // Update the device list
        UpdateDevices();
    });
}

private async void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation args)
{
    await coreDispatcher.RunAsync(CoreDispatcherPriority.High, () =>
    {
        // Update the device list
        UpdateDevices();
    });
}

private async void DeviceWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
{
    await coreDispatcher.RunAsync(CoreDispatcherPriority.High, () =>
    {
        // Update the device list
        UpdateDevices();
    });
}

private async void DeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
{
    await coreDispatcher.RunAsync(CoreDispatcherPriority.High, () =>
    {
        // Update the device list
        UpdateDevices();
    });
}

UpdateDevices 帮助程序方法调用 DeviceInformation.FindAllAsync 并通过已返回设备的名称更新 ListBox,如本文的前面部分所述。

private async void UpdateDevices()
{
    // Get a list of all MIDI devices
    this.DeviceInformationCollection = await DeviceInformation.FindAllAsync(deviceSelectorString);

    deviceListBox.Items.Clear();

    if (!this.DeviceInformationCollection.Any())
    {
        deviceListBox.Items.Add("No MIDI devices found!");
    }

    foreach (var deviceInformation in this.DeviceInformationCollection)
    {
        deviceListBox.Items.Add(deviceInformation.Name);
    }
}

添加多个方法,若要启动观察程序,使用 DeviceWatcher 对象的 Start 方法;若要停止观察程序,使用 Stop 方法。

public void StartWatcher()
{
    deviceWatcher.Start();
}
public void StopWatcher()
{
    deviceWatcher.Stop();
}

提供析构函数以取消注册观察程序事件处理程序,并将设备观察程序设置为 null。

~MyMidiDeviceWatcher()
{
    deviceWatcher.Added -= DeviceWatcher_Added;
    deviceWatcher.Removed -= DeviceWatcher_Removed;
    deviceWatcher.Updated -= DeviceWatcher_Updated;
    deviceWatcher.EnumerationCompleted -= DeviceWatcher_EnumerationCompleted;
    deviceWatcher = null;
}

创建 MIDI 端口以发送和接收消息

在页面的代码隐藏中,声明成员变量以保留 MyMidiDeviceWatcher 帮助程序类的两个实例:一个用于输入设备,一个用于输出设备。

MyMidiDeviceWatcher inputDeviceWatcher;
MyMidiDeviceWatcher outputDeviceWatcher;

创建观察程序帮助程序类的新实例,从而传入设备选择器字符串、要填充的 ListBox,以及可通过页面的 Dispatcher 属性访问的 CoreDispatcher 对象。 然后,调用该方法以启动每个对象的 DeviceWatcher

在启动每个 DeviceWatcher 后不久,它将完成枚举连接到系统的当前设备,并引发其 EnumerationCompleted 事件,这将导致使用当前 MIDI 设备更新每个 ListBox

inputDeviceWatcher =
    new MyMidiDeviceWatcher(MidiInPort.GetDeviceSelector(), midiInPortListBox, Dispatcher);

inputDeviceWatcher.StartWatcher();

outputDeviceWatcher =
    new MyMidiDeviceWatcher(MidiOutPort.GetDeviceSelector(), midiOutPortListBox, Dispatcher);

outputDeviceWatcher.StartWatcher();

当用户在 MIDI 输入 ListBox 中选择某一项时,将引发 SelectionChanged 事件。 在此事件的处理程序中,访问帮助程序类的 DeviceInformationCollection 属性以获取当前设备列表。 如果列表中存在条目,使用对应于 ListBox 控件 SelectedIndex 的索引选择 DeviceInformation 对象。

通过调用 MidiInPort.FromIdAsync 创建表示所选输入设备的 MidiInPort 对象,从而传入所选设备的 Id 属性。

MessageReceived 事件注册处理程序,该事件将在通过指定的设备接收 MIDI 消息时引发。

MidiInPort midiInPort;
IMidiOutPort midiOutPort;
private async void midiInPortListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var deviceInformationCollection = inputDeviceWatcher.DeviceInformationCollection;

    if (deviceInformationCollection == null)
    {
        return;
    }

    DeviceInformation devInfo = deviceInformationCollection[midiInPortListBox.SelectedIndex];

    if (devInfo == null)
    {
        return;
    }

    midiInPort = await MidiInPort.FromIdAsync(devInfo.Id);

    if (midiInPort == null)
    {
        System.Diagnostics.Debug.WriteLine("Unable to create MidiInPort from input device");
        return;
    }
    midiInPort.MessageReceived += MidiInPort_MessageReceived;
}

调用 MessageReceived 处理程序时,该消息将包含在 MidiMessageReceivedEventArgsMessage 属性中。 消息对象的 TypeMidiMessageType 枚举中的值,用于指示已接收的消息类型。 消息数据取决于消息类型。 此示例可查看该消息是否是消息相关注释;如果是,则输出该消息的 MIDI 通道、注释和速度。

private void MidiInPort_MessageReceived(MidiInPort sender, MidiMessageReceivedEventArgs args)
{
    IMidiMessage receivedMidiMessage = args.Message;

    System.Diagnostics.Debug.WriteLine(receivedMidiMessage.Timestamp.ToString());

    if (receivedMidiMessage.Type == MidiMessageType.NoteOn)
    {
        System.Diagnostics.Debug.WriteLine(((MidiNoteOnMessage)receivedMidiMessage).Channel);
        System.Diagnostics.Debug.WriteLine(((MidiNoteOnMessage)receivedMidiMessage).Note);
        System.Diagnostics.Debug.WriteLine(((MidiNoteOnMessage)receivedMidiMessage).Velocity);
    }
}

输出设备 ListBoxSelectionChanged 处理程序与输入设备的处理程序的工作原理相同,不同之处在于未注册任何事件处理程序。

private async void midiOutPortListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var deviceInformationCollection = outputDeviceWatcher.DeviceInformationCollection;

    if (deviceInformationCollection == null)
    {
        return;
    }

    DeviceInformation devInfo = deviceInformationCollection[midiOutPortListBox.SelectedIndex];

    if (devInfo == null)
    {
        return;
    }

    midiOutPort = await MidiOutPort.FromIdAsync(devInfo.Id);

    if (midiOutPort == null)
    {
        System.Diagnostics.Debug.WriteLine("Unable to create MidiOutPort from output device");
        return;
    }

}

创建输出设备后,通过针对要发送的消息类型创建新的 IMidiMessage,可以发送一条消息。 在此示例中,该消息为 NoteOnMessage。 调用 IMidiOutPort 对象的 SendMessage 方法以发送消息。

byte channel = 0;
byte note = 60;
byte velocity = 127;
IMidiMessage midiMessageToSend = new MidiNoteOnMessage(channel, note, velocity);

midiOutPort.SendMessage(midiMessageToSend);

当你的应用停用时,请务必清理你的应用资源。 取消注册你的事件处理程序,并将 MIDI 输入端口和输出端口对象设置为 null。 停止设备观察程序并将其设置为 null。

inputDeviceWatcher.StopWatcher();
inputDeviceWatcher = null;

outputDeviceWatcher.StopWatcher();
outputDeviceWatcher = null;

midiInPort.MessageReceived -= MidiInPort_MessageReceived;
midiInPort.Dispose();
midiInPort = null;

midiOutPort.Dispose();
midiOutPort = null;

使用内置的 Windows 通用 MIDI 合成器

当你使用上面所述的技术枚举输出 MIDI 设备时,你的应用会发现一个名为“Microsoft GS 波形表合成器”的 MIDI 设备。 这是一款你可以从应用中使用的内置通用 MIDI 合成器。 但是,如果没有将该内置合成器的 SDK 扩展包含在你的项目中,将无法为此设备创建 MIDI 输出端口。

将通用 MIDI 合成器 SDK 扩展包含在应用项目中

  1. “解决方案资源管理器”中,在你的项目下,右键单击“引用”,然后选择“添加引用...”
  2. 展开“通用 Windows”节点。
  3. 选择“扩展”。
  4. 从扩展列表中,选择“适用于通用 Windows 应用的 Microsoft 通用 MIDI DLS”

    注意

    如果存在多个版本的扩展,请务必选择与应用所面向的目标匹配的版本。 你可以在项目“属性”的“应用程序”选项卡上查看应用所面向的 SDK 版本。