MIDI

W tym artykule pokazano, jak wyliczać urządzenia MIDI (Musical Instrument Digital Interface) i wysyłać i odbierać komunikaty MIDI z aplikacji WinUI. Windows obsługuje MIDI przez USB (urządzenia zgodne ze standardem class-compliant oraz większość sterowników własnościowych), MIDI przez Bluetooth LE, a także — za pośrednictwem ogólnodostępnych rozwiązań firm trzecich — MIDI przez Ethernet i trasowane MIDI.

Utwórz klasę pomocniczą obserwatora urządzeń

Przestrzeń nazw Windows.Devices.Enumeration udostępnia DeviceWatcher, który może powiadamiać aplikację, jeśli urządzenia zostaną dodane do systemu lub z niego usunięte albo jeśli informacje o urządzeniu zostaną zaktualizowane. Ponieważ aplikacje z obsługą midi są zwykle zainteresowane zarówno urządzeniami wejściowymi, jak i wyjściowymi, ten przykład tworzy klasę pomocnika, która implementuje wzorzec DeviceWatcher , dzięki czemu ten sam kod może być używany zarówno dla urządzeń wejściowych MIDI, jak i wyjściowych MIDI, bez konieczności duplikowania.

Dodaj do projektu nową klasę, która będzie monitorować urządzenie. W tym przykładzie klasa nosi nazwę MidiDeviceWatcher. Pozostała część kodu w tej sekcji służy do implementowania klasy pomocniczej.

Dodaj kilka zmiennych składowych do klasy:

  • Obiekt DeviceWatcher monitorujący zmiany urządzenia.
  • Ciąg wyboru urządzenia, który dla jednego wystąpienia będzie zawierać ciąg wyboru portu wejściowego MIDI, a dla innego wystąpienia ciąg wyboru portu wyjściowego MIDI.
  • Kontrolka ListBox , która zostanie wypełniona nazwami dostępnych urządzeń.
  • DispatcherQueue wymagana do aktualizowania interfejsu użytkownika z wątku innego niż wątek UI.
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
DispatcherQueue dispatcherQueue;

Dodaj właściwość DeviceInformationCollection używana do uzyskiwania dostępu do bieżącej listy urządzeń spoza klasy pomocniczej.

public DeviceInformationCollection? DeviceInformationCollection { get; set; }

W konstruktorze klasy kod wywołujący przekazuje ciąg selektora urządzenia MIDI, element ListBox do wyświetlania urządzeń na liście oraz obiekt DispatcherQueue potrzebny do aktualizowania interfejsu użytkownika.

Wywołaj metodę DeviceInformation.CreateWatcher , aby utworzyć nowe wystąpienie klasy DeviceWatcher , przekazując ciąg selektora urządzeń MIDI.

Zarejestruj programy obsługi zdarzeń obserwatora.

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

Urządzenie DeviceWatcher ma następujące zdarzenia:

W procedurze obsługi każdego z tych zdarzeń wywoływana jest metoda pomocnicza UpdateDevices, aby zaktualizować kontrolkę ListBox o bieżącą listę urządzeń. Ponieważ updateDevices aktualizuje elementy interfejsu użytkownika, a te programy obsługi zdarzeń nie są wywoływane w wątku interfejsu użytkownika, każde wywołanie musi być opakowane w wywołanie metody 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();
    });
}

Metoda pomocnika UpdateDevices wywołuje metodę DeviceInformation.FindAllAsync i aktualizuje element ListBox przy użyciu nazw zwróconych urządzeń zgodnie z opisem w tym artykule.

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

Dodaj metody uruchamiania obserwatora przy użyciu metody Start obiektu DeviceWatcher i zatrzymania obserwatora przy użyciu metody Stop.

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

Podaj destruktor, aby wyrejestrować programy obsługi zdarzeń obserwatora i ustawić obserwatorowi urządzenia wartość null.

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

Tworzenie portów MIDI do wysyłania i odbierania komunikatów

W kodzie źródłowym powiązanym z oknem zadeklaruj zmienne członkowskie służące do przechowywania dwóch instancji klasy pomocniczej MidiDeviceWatcher, jednej dla urządzeń wejściowych i jednej dla urządzeń wyjściowych.

MidiDeviceWatcher? inputDeviceWatcher;
MidiDeviceWatcher? outputDeviceWatcher;

Zadeklaruj również zmienne składowe dla obiektów portów wejściowych i wyjściowych MIDI.

MidiInPort? midiInPort;
IMidiOutPort? midiOutPort;

Utwórz nową instancję klas pomocniczych obserwatora, przekazując ciąg znaków selektora urządzenia, element ListBox, który ma zostać wypełniony, oraz obiekt DispatcherQueue. Następnie wywołaj metodę , aby uruchomić urządzenie DeviceWatcher każdego obiektu.

Wkrótce po uruchomieniu urządzenia DeviceWatcher zakończy wyliczanie bieżących urządzeń połączonych z systemem i zgłosi zdarzenie EnumerationCompleted , co spowoduje zaktualizowanie każdego urządzenia ListBox przy użyciu bieżących urządzeń MIDI.

inputDeviceWatcher =
    new MidiDeviceWatcher(MidiInPort.GetDeviceSelector(), midiInPortListBox, DispatcherQueue);

inputDeviceWatcher.StartWatcher();

outputDeviceWatcher =
    new MidiDeviceWatcher(MidiOutPort.GetDeviceSelector(), midiOutPortListBox, DispatcherQueue);

outputDeviceWatcher.StartWatcher();

Gdy użytkownik wybierze element w polu listy ListBox wejścia MIDI, zostanie zgłoszone zdarzenie SelectionChanged. W programie obsługi dla tego zdarzenia uzyskaj dostęp do właściwości DeviceInformationCollection klasy pomocniczej, aby uzyskać bieżącą listę urządzeń. Jeśli na liście znajdują się wpisy, wybierz obiekt DeviceInformation z indeksem odpowiadającym wybranemu indeksowi kontrolki ListBox.

Utwórz obiekt MidiInPort reprezentujący wybrane urządzenie wejściowe, wywołując MidiInPort.FromIdAsync i przekazując właściwość Id wybranego urządzenia.

Zarejestruj program obsługi zdarzenia MessageReceived , który jest zgłaszany za każdym razem, gdy komunikat MIDI jest odbierany za pośrednictwem określonego urządzenia.

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

Gdy wywoływany jest program obsługi MessageReceived, komunikat znajduje się we właściwości Message klasy MidiMessageReceivedEventArgs. Właściwość Typ obiektu wiadomości jest wartością wyliczenia MidiMessageType, wskazującą typ odebranej wiadomości. Dane komunikatu zależą od typu komunikatu. W tym przykładzie sprawdza się, czy komunikat jest notatką dotyczącą komunikatu, a jeśli tak, wyprowadza kanał midi, notatkę i szybkość komunikatu.

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

Procedura obsługi SelectionChanged dla elementu ListBox urządzenia wyjściowego działa tak samo jak procedura obsługi dla urządzeń wejściowych, z tą różnicą, że nie jest rejestrowana żadna procedura obsługi zdarzenia.

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

Po utworzeniu urządzenia wyjściowego możesz wysłać komunikat, tworząc nowy IMidiMessage dla typu wiadomości, którą chcesz wysłać. W tym przykładzie komunikat jest NoteOnMessage. Obiekt SendMessage metody IMidiOutPort jest wywoływany w celu wysłania komunikatu.

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

midiOutPort.SendMessage(midiMessageToSend);

Po zamknięciu aplikacji pamiętaj, aby wyczyścić zasoby aplikacji. Wyrejestruj procedury obsługi zdarzeń i ustaw obiekty portu wejściowego i wyjściowego MIDI na wartość null. Zatrzymaj obserwatorów urządzeń i ustaw je na wartość 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;
}

Korzystanie z wbudowanego syntezatora General MIDI systemu Windows

Podczas wyliczania wyjściowych urządzeń MIDI przy użyciu opisanej powyżej techniki aplikacja odnajdzie urządzenie MIDI o nazwie "Microsoft GS Wavetable Synth". Jest to wbudowany syntetyzator GENERAL MIDI, który można odtworzyć z aplikacji.

Zestaw SDK rozszerzeń UWP dla General MIDI („Microsoft General MIDI DLS for Universal Windows Apps”) nie jest dostępny w projektach WinUI 3. Stare okno dialogowe Dodawanie rozszerzeń referencji > było przeznaczone wyłącznie dla platformy UWP. Jednak w większości scenariuszy na komputerach stacjonarnych syntezator GS Wavetable działa bez zestawu SDK rozszerzenia, ponieważ aplikacje desktopowe mogą uzyskać bezpośredni dostęp do systemowego banku dźwięków gm.dls. Jeśli okaże się, że wyjście MIDI nie generuje dźwięku, sprawdź, czy urządzenie wyjściowe MIDI jest zaznaczone i czy dane wyjściowe audio są poprawnie skonfigurowane.