MIDI (Interfaccia Digitale per Strumenti Musicali)

Questo articolo illustra come enumerare i dispositivi MIDI (Musical Instrument Digital Interface) e inviare e ricevere messaggi MIDI da un'app WinUI. Windows supporta MIDI su USB (driver conformi alla classe e più proprietari), MIDI tramite Bluetooth LE e attraverso prodotti di terze parti disponibili gratuitamente, MIDI su Ethernet e MIDI indirizzato.

Creare una classe di supporto per l'osservatore di dispositivi

Windows. Devices.Enumeration fornisce lo spazio dei nomi DeviceWatcher che può notificare all'app se i dispositivi vengono aggiunti o rimossi dal sistema o se le informazioni per un dispositivo vengono aggiornate. Poiché le app abilitate per MIDI sono in genere interessate sia ai dispositivi di input che di output, questo esempio crea una classe helper che implementa il modello DeviceWatcher , in modo che lo stesso codice possa essere usato sia per i dispositivi di input MIDI che per i dispositivi di output MIDI, senza la necessità di duplicazione.

Aggiungi una nuova classe al progetto che funga da monitor del dispositivo. In questo esempio la classe è denominata MidiDeviceWatcher. Il resto del codice in questa sezione viene usato per implementare la classe helper.

Aggiungere alcune variabili membro alla classe :

  • Oggetto DeviceWatcher che monitorerà le modifiche del dispositivo.
  • Una stringa del selettore del dispositivo che conterrà, in un'istanza, la stringa del selettore della porta di ingresso MIDI e, in un'altra istanza, la stringa del selettore della porta di uscita MIDI.
  • Controllo ListBox che verrà popolato con i nomi dei dispositivi disponibili.
  • Un DispatcherQueue necessario per aggiornare l'interfaccia utente da un thread diverso da quello dell'interfaccia utente.
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
DispatcherQueue dispatcherQueue;

Aggiungere una proprietà DeviceInformationCollection usata per accedere all'elenco corrente di dispositivi dall'esterno della classe helper.

public DeviceInformationCollection? DeviceInformationCollection { get; set; }

Nel costruttore della classe, il chiamante passa la stringa del selettore del dispositivo MIDI, la ListBox per elencare i dispositivi e la DispatcherQueue necessaria per aggiornare l'interfaccia utente.

Chiama DeviceInformation.CreateWatcher per creare una nuova istanza della classe DeviceWatcher , passando la stringa del selettore del dispositivo MIDI.

Registrare i gestori per i gestori eventi del watcher.

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 presenta gli eventi seguenti:

  • Aggiunto : generato quando viene aggiunto un nuovo dispositivo al sistema.
  • Rimosso : generato quando un dispositivo viene rimosso dal sistema.
  • Aggiornamento - Si verifica quando vengono aggiornate le informazioni associate a un dispositivo esistente.
  • EnumerationCompleted - Generato quando il watcher ha completato l'enumerazione del tipo di dispositivo richiesto.

Nel gestore eventi per ognuno di questi eventi viene chiamato un metodo helper UpdateDevices per aggiornare ListBox con l'elenco corrente di dispositivi. Poiché UpdateDevices aggiorna gli elementi dell'interfaccia utente e questi gestori eventi non vengono chiamati nel thread dell'interfaccia utente, ogni chiamata deve essere sottoposta a wrapping in una chiamata a 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();
    });
}

Il metodo helper UpdateDevices chiama DeviceInformation.FindAllAsync e aggiorna ListBox con i nomi dei dispositivi restituiti come descritto in precedenza in questo articolo.

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

Aggiungere metodi per avviare il watcher, usando il metodo Start dell'oggetto DeviceWatcher e per arrestare il watcher usando il metodo Stop.

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

Definire un distruttore per annullare la registrazione dei gestori eventi del watcher e impostare il watcher del dispositivo su null.

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

Creare porte MIDI per inviare e ricevere messaggi

Nel code-behind della finestra, dichiarate variabili membro che contengano due istanze della classe helper MidiDeviceWatcher, una per i dispositivi di input e una per i dispositivi di output.

MidiDeviceWatcher? inputDeviceWatcher;
MidiDeviceWatcher? outputDeviceWatcher;

Dichiarate anche le variabili membro per gli oggetti delle porte di input e output MIDI.

MidiInPort? midiInPort;
IMidiOutPort? midiOutPort;

Crea una nuova istanza delle classi helper del watcher, passando la stringa del selettore del dispositivo, il ListBox da popolare e l'oggetto DispatcherQueue. Chiamare quindi il metodo per avviare DeviceWatcher di ogni oggetto.

Poco dopo l'avvio di ogni DeviceWatcher , termina l'enumerazione dei dispositivi correnti connessi al sistema e genera l'evento EnumerationCompleted , che causerà l'aggiornamento di ogni ListBox con i dispositivi MIDI correnti.

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

inputDeviceWatcher.StartWatcher();

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

outputDeviceWatcher.StartWatcher();

Quando l'utente seleziona un elemento nell'input MIDI ListBox, viene generato l'evento SelectionChanged . Nel gestore per questo evento accedere alla proprietà DeviceInformationCollection della classe helper per ottenere l'elenco corrente dei dispositivi. Se sono presenti voci nell'elenco, selezionare l'oggetto DeviceInformation con l'indice corrispondente all'oggetto SelectedIndex del controllo ListBox.

Creare l'oggetto MidiInPort che rappresenta il dispositivo di input selezionato chiamando MidiInPort.FromIdAsync passando la proprietà Id del dispositivo selezionato.

Registrare un gestore per l'evento MessageReceived , che viene generato ogni volta che viene ricevuto un messaggio MIDI tramite il dispositivo specificato.

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

Quando viene chiamato il gestore MessageReceived, il messaggio è contenuto nella proprietà Message della proprietà MidiMessageReceivedEventArgs. L'enumerazione Type dell'oggetto messaggio è un valore dell'enumerazione MidiMessageType che indica il tipo di messaggio ricevuto. I dati del messaggio dipendono dal tipo di messaggio. In questo esempio viene verificato se il messaggio è una nota sul messaggio e, in tal caso, restituisce il canale midi, la nota e la velocità del messaggio.

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

Il gestore SelectionChanged per il dispositivo di output ListBox funziona come il gestore per i dispositivi di input, ad eccezione di nessun gestore eventi registrato.

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

Dopo aver creato il dispositivo di output, è possibile inviare un messaggio creando un nuovo IMidiMessage per il tipo di messaggio da inviare. In questo esempio il messaggio è un NoteOnMessage. Il metodo SendMessage dell'oggetto IMidiOutPort viene chiamato per inviare il messaggio.

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

midiOutPort.SendMessage(midiMessageToSend);

Quando l'app è chiusa, assicurarsi di pulire le risorse dell'app. Annulla la registrazione dei gestori di eventi e imposta su null gli oggetti della porta di ingresso MIDI e della porta di uscita. Interrompere gli osservatori del dispositivo e impostarli su 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;
}

Utilizzo del sintetizzatore General MIDI integrato in Windows

Quando enumererai i dispositivi MIDI di output usando la tecnica descritta in precedenza, la tua app scoprirà un dispositivo MIDI denominato "Microsoft GS Wavetable Synth". Si tratta di un sintetizzatore MIDI generale predefinito che puoi riprodurre dalla tua app.

L'SDK dell'estensione UWP per general MIDI ("Microsoft General MIDI DLS for Universal Windows Apps") non è disponibile nei progetti WinUI 3. La finestra di dialogo Aggiungi estensioni di riferimento > precedente era specifica per la piattaforma UWP. Tuttavia, per la maggior parte degli scenari desktop, GS Wavetable Synth funziona senza l'SDK di estensione perché le app desktop possono accedere direttamente alla banca audio del gm.dls sistema. Se trovi che l'output MIDI non produce suoni, verifica che sia selezionato un dispositivo di output MIDI e che l'output audio sia configurato correttamente.