MIDI

В этой статье показано, как перечислять MIDI-устройства (Musical Instrument Digital Interface), а также отправлять и получать сообщения MIDI из универсального приложения для Windows. Windows 10 поддерживает MIDI через USB (совместимые с классами и большинство собственных драйверов), MIDI через Bluetooth LE (Windows 10 Anniversary Edition и более поздних версий), а также через свободно доступные сторонние продукты, MIDI через Ethernet и перенаправленные MIDI.

Перечисление MIDI-устройств

Перед перечислением и использованием MIDI-устройств добавьте в свой проект следующие пространства имен.

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

Добавьте на свою XAML-страницу элемент управления ListBox, который позволит пользователю выбрать одно из подключенных к системе устройств ввода MIDI. Добавьте другой элемент управления для списка устройств вывода MIDI.

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

Класс DeviceInformation метода FindAllAsync используется для перечисления различных типов устройств, обнаруженных Windows. Чтобы указать, что вам требуется только метод для поиска устройств ввода MIDI, используйте строку селектора, возвращенную MidiInPort.GetDeviceSelector. FindAllAsync возвращает коллекцию DeviceInformationCollection, которая содержит DeviceInformation для всех зарегистрированных в системе устройств ввода MIDI. Если возвращаемая коллекция не содержит ни одного элемента, то доступные устройства ввода MIDI отсутствуют. Если в коллекции есть элементы, создайте цикл по объектам DeviceInformation и добавьте имя каждого устройства в список ListBox для устройств ввода MIDI.

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 работает аналогичным образом, за исключением того, что вам следует указать строку селектора, возвращенную MidiOutPort.GetDeviceSelector при вызове FindAllAsync.

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 можно использовать один и тот же код, не прибегая к дублированию.

Добавьте в свой проект новый класс, выступающий в качестве наблюдателя устройств. В данном примере он называется MyMidiDeviceWatcher. Остальная часть кода в этом разделе используется для реализации вспомогательного класса.

Добавьте в класс несколько переменных-членов:

  • Объект DeviceWatcher, который будет отслеживать изменения устройств.
  • Строку селектора устройства, которая будет содержать строку селектора входного MIDI-порта для одного экземпляра и строку селектора выходного MIDI-порта для другого экземпляра.
  • Элемент управления ListBox, который будет заполнен именами доступных устройств.
  • CoreDispatcher, необходимый для обновления пользовательского интерфейса из потока, отличного от потока пользовательского интерфейса.
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
CoreDispatcher coreDispatcher;

Добавьте свойство DeviceInformationCollection, которое используется для получения доступа к текущему списку устройств из внешнего вспомогательного класса.

public DeviceInformationCollection DeviceInformationCollection { get; set; }

В конструкторе класса вызывающая сторона передает строку селектора MIDI-устройства, объект ListBox со списком устройств и объект 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 обновляет элементы пользовательского интерфейса, а эти обработчики событий не вызываются в потоке пользовательского интерфейса, каждый вызов должен быть заключен в вызов RunAsync, который запускает указанный код в потоке пользовательского интерфейса.

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);
    }
}

Добавьте методы для запуска наблюдателя с помощью метода Start объекта DeviceWatcher и для остановки наблюдателя с помощью метода 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, и объект CoreDispatcher, который может быть получен через свойство Dispatcher этой страницы. Затем вызовите этот метод, чтобы запустить DeviceWatcher каждого объекта.

Вскоре после запуска каждого объекта DeviceWatcher он завершит перечисление устройств, которые в настоящий момент подключены к системе, и вызовет его событие EnumerationCompleted, которое обновит список текущих устройств MIDI в каждом объекте ListBox.

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

inputDeviceWatcher.StartWatcher();

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

outputDeviceWatcher.StartWatcher();

Когда пользователь выбирает элемент из списка ListBox входных устройств MIDI, возникает событие SelectionChanged. В обработчике для этого события обратитесь к свойству DeviceInformationCollection вспомогательного класса, чтобы получить текущий список устройств. Если в списке есть записи, выберите объект DeviceInformation с помощью индекса, соответствующего SelectedIndex элемента управления ListBox.

Создайте объект MidiInPort, представляющий выбранное устройство ввода, вызвав MidiInPort.FromIdAsync и передав свойство 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, это сообщение содержится в свойстве Message для MidiMessageReceivedEventArgs. 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);
    }
}

Обработчик 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);

Не забудьте очистить ресурсы приложения после его деактивации. Отмените регистрацию обработчиков событий и установите значение NULL для объектов входного и выходного MIDI-порта. Остановите наблюдатели устройств и установите их в значение 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, используя описанный выше способ, ваше приложение обнаружит MIDI-устройство «Microsoft GS Wavetable Synth». Это встроенный синтезатор General MIDI, который можно использовать в приложении. Однако если попытаться создать выходной порт MIDI для этого устройства, возникнет ошибка, если вы не включили расширение SDK для встроенного синтезатора в свой проект.

Включение расширения SDK синтезатора General MIDI в проект приложения

  1. В обозревателе решений щелкните правой кнопкой мыши Ссылки и выберите Добавить ссылку....
  2. Разверните узел Универсальное приложение для Windows.
  3. Выберите Расширения.
  4. В списке расширений выберите Microsoft General MIDI DLS для универсальных приложений для Windows.

    Примечание

    Если доступно несколько версий расширения, выберите ту из них, которая соответствует целевой версии SDK вашего приложения. Вы можете узнать, для какой версии SDK предназначено приложение на вкладке Приложение свойств проекта.