本文介绍如何枚举 MIDI(乐器数字接口)设备和从 WinUI 应用发送和接收 MIDI 消息。 Windows 支持通过 USB 传输的 MIDI(类兼容和大多数专有驱动程序)、通过蓝牙 LE 传输的 MIDI,以及借助免费提供的第三方产品实现的通过以太网传输的 MIDI 和路由 MIDI。
创建设备监视器帮助器类
Windows.Devices.Enumeration 命名空间提供 DeviceWatcher,它可在设备已添加到系统、已从系统中删除,或设备信息已更新时通知你的应用。 由于启用了 MIDI 的应用通常对输入和输出设备感兴趣,因此此示例会创建一个实现 DeviceWatcher 模式的帮助程序类,以便同一代码可用于 MIDI 输入和 MIDI 输出设备,而无需重复。
向项目中添加一个新类,作为设备监视器。 在此示例中,类名为 MidiDeviceWatcher。 本节中的其余代码用于实现帮助程序类。
向类中添加一些成员变量:
- DeviceWatcher 对象,用于监视设备更改。
- 一个设备选择器字符串,在一个实例中将包含 MIDI 输入端口选择器字符串,在另一个实例中将包含 MIDI 输出端口选择器字符串。
- 一个 ListBox 控件,该控件将填充可用设备的名称。
- 用于从非 UI 线程更新 UI 所需的 DispatcherQueue。
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
DispatcherQueue dispatcherQueue;
添加一个 DeviceInformationCollection 属性,该属性用于从帮助程序类外部访问设备的当前列表。
public DeviceInformationCollection? DeviceInformationCollection { get; set; }
在类构造函数中,调用方传入 MIDI 设备选择器字符串、 列出设备的 ListBox 以及更新 UI 所需的 DispatcherQueue 。
调用 DeviceInformation.CreateWatcher 以创建 DeviceWatcher 类的新实例,传入 MIDI 设备选择器字符串。
为监视器的事件处理程序注册处理程序。
public MidiDeviceWatcher(string midiDeviceSelectorString, ListBox midiDeviceListBox, DispatcherQueue dispatcher)
{
deviceListBox = midiDeviceListBox;
dispatcherQueue = dispatcher;
deviceSelectorString = midiDeviceSelectorString;
deviceWatcher = DeviceInformation.CreateWatcher(deviceSelectorString);
deviceWatcher.Added += DeviceWatcher_Added;
deviceWatcher.Removed += DeviceWatcher_Removed;
deviceWatcher.Updated += DeviceWatcher_Updated;
deviceWatcher.EnumerationCompleted += DeviceWatcher_EnumerationCompleted;
}
DeviceWatcher 具有以下事件:
- 已添加 - 当新设备添加到系统时触发。
- 已删除 - 从系统中删除设备时引发。
- 更新 - 当与现有设备关联的信息被更新时触发。
- EnumerationCompleted - 当观察程序完成请求的设备类型的枚举时引发。
在上述每个事件的事件处理程序中,将调用帮助程序方法 UpdateDevices,以使用当前设备列表更新 ListBox 。 由于 UpdateDevices 更新 UI 元素,并且不会在 UI 线程上调用这些事件处理程序,因此每个调用都必须包装在 对 DispatcherQueue.TryEnqueue 的调用中。
private void DeviceWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
{
dispatcherQueue.TryEnqueue(() =>
{
UpdateDevices();
});
}
private void DeviceWatcher_Added(DeviceWatcher sender, DeviceInformation args)
{
dispatcherQueue.TryEnqueue(() =>
{
UpdateDevices();
});
}
private void DeviceWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
{
dispatcherQueue.TryEnqueue(() =>
{
UpdateDevices();
});
}
private void DeviceWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
{
dispatcherQueue.TryEnqueue(() =>
{
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。
~MidiDeviceWatcher()
{
deviceWatcher.Added -= DeviceWatcher_Added;
deviceWatcher.Removed -= DeviceWatcher_Removed;
deviceWatcher.Updated -= DeviceWatcher_Updated;
deviceWatcher.EnumerationCompleted -= DeviceWatcher_EnumerationCompleted;
}
创建 MIDI 端口以发送和接收消息
在窗口的后台代码中,声明成员变量,用于保存 MidiDeviceWatcher 辅助类的两个实例,一个用于输入设备,另一个用于输出设备。
MidiDeviceWatcher? inputDeviceWatcher;
MidiDeviceWatcher? outputDeviceWatcher;
同时声明 MIDI 输入和输出端口对象的成员变量。
MidiInPort? midiInPort;
IMidiOutPort? midiOutPort;
创建一个新的监视器帮助程序类实例,并传入设备选择器字符串、要填充的 ListBox 以及 DispatcherQueue 对象。 然后,调用该方法以启动每个对象的 DeviceWatcher。
启动每个 DeviceWatcher 后不久,它将完成枚举连接到系统的当前设备并引发其 EnumerationCompleted 事件,这将导致每个 ListBox 更新为当前 MIDI 设备。
inputDeviceWatcher =
new MidiDeviceWatcher(MidiInPort.GetDeviceSelector(), midiInPortListBox, DispatcherQueue);
inputDeviceWatcher.StartWatcher();
outputDeviceWatcher =
new MidiDeviceWatcher(MidiOutPort.GetDeviceSelector(), midiOutPortListBox, DispatcherQueue);
outputDeviceWatcher.StartWatcher();
当用户在 MIDI 输入 ListBox 中选择项时,将引发 SelectionChanged 事件。 在此事件的处理程序中,访问帮助程序类的 DeviceInformationCollection 属性以获取设备的当前列表。 如果列表中存在条目,请选择具有与 ListBox 控件 SelectedIndex 对应的索引的 DeviceInformation 对象。
通过调用 MidiInPort.FromIdAsync 并传入所选设备的 Id 属性,创建表示所选输入设备的 MidiInPort 对象。
为 MessageReceived 事件注册处理程序,每当通过指定设备接收 MIDI 消息时,将引发该事件。
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 处理程序时, 消息包含在 Message 属性中MidiMessageReceivedEventArgs。 消息对象的 Type 是来自 MidiMessageType 枚举的值,指示收到的消息的类型。 消息的数据取决于消息的类型。 此示例检查该消息是否为 Note On 消息,如果是,则输出该消息的 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);
}
}
输出设备的 SelectionChanged 处理程序 ListBox 的工作方式与输入设备的处理程序相同,但未注册任何事件处理程序。
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。 调用 sendMessage 方法IMidiOutPort 对象发送消息。
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;
if (midiInPort != null)
{
midiInPort.MessageReceived -= MidiInPort_MessageReceived;
midiInPort.Dispose();
midiInPort = null;
}
if (midiOutPort != null)
{
midiOutPort.Dispose();
midiOutPort = null;
}
使用 Windows 内置的 General MIDI 合成器
使用上述技术枚举输出 MIDI 设备时,应用将发现名为“Microsoft GS Wavetable Synth”的 MIDI 设备。 这是可从应用播放的内置常规 MIDI 合成器。
适用于常规 MIDI 的 UWP 扩展 SDK(“适用于通用Windows应用的通用 MIDI DLS Microsoft”)在 WinUI 3 项目中不可用。 旧的 “添加引用 > 扩展 ”对话框特定于 UWP。 但是,对于大多数桌面方案,GS Wavetable Synth 在没有扩展 SDK 的情况下工作,因为桌面应用可以直接访问系统的 gm.dls 声库。 如果发现 MIDI 输出未生成声音,请验证是否选择了 MIDI 输出设备,并确保音频输出已正确配置。