MIDI
本文介紹如何列舉 MIDI (樂器數位介面) 裝置,以及如何從通用 Windows 應用傳送和接收 MIDI 訊息。 Windows 10 支援 USB 上的 MIDI (符合類別標準和大多數專有驅動程式)、藍牙 LE 上的MIDI (Windows 10 週年紀念版及更高版本) 以及透過免費提供的第三方產品、乙太網路上的 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 傳回一個 DeviceInformationCollection,其中包含向系統註冊的每個 MIDI 輸入裝置的 DeviceInformation。 如果傳回的集合不包含任何項目,則沒有可用的 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 控制項,會填入可用裝置的名稱。
- CoreDispatcher 是從 UI 執行緒以外的執行緒更新 UI 所需的。
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。 因為 UpdateDevices 會更新 UI 元素,而且不會在 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 事件,這將導致每個 ListBox 使用目前 MIDI 裝置進行更新。
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 並傳入所選裝置的 Id 屬性,建立表示所選輸入裝置的 MidiInPort 物件。
註冊 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 處理常式時,訊息包含在 MidiMessageReceivedEventArgs 的 Message 屬性中。 訊息物件的 Type 是 MidiMessageType 列舉中的一個值,指示收到的訊息類型。 訊息的資料取決於訊息的類型。 此範例會檢查訊息是否為訊息上的附註,如果是,則會輸出訊息的 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);
}
}
輸出裝置 ListBox 的 SelectionChanged 處理常式的工作方式與輸入裝置的處理常式相同,但沒有註冊事件處理常式。
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 General MIDI 合成器
當您使用上述技術列舉輸出 MIDI 裝置時,您的應用程式會探索名為「Microsoft GS Wavetable Synth」的 MIDI 裝置。 這是您可以從應用程式播放的內建一般 MIDI 合成器。 不過,除非您已在專案中包含內建合成器 SDK 延伸模組,否則嘗試建立此裝置的 MIDI 輸出將會失敗。
在您的應用程式專案中加入一般 MIDI 合成器 SDK 延伸模組
- 在方案總管中,於您的應用程式專案下,以滑鼠右鍵按一下參考,然後選取新增參考...
- 展開通用 Windows 節點。
- 選取 [延伸模組]。
- 從延伸模組清單中,選取 Microsoft General MIDI DLS for Universal Windows 應用程式。
注意
如果延伸模組有多個版本,請務必選取符合您應用程式目標的版本。 您可以在專案屬性的應用程式標籤上查看您的應用程式所針對的 SDK 版本。