MIDI

Este artigo mostra como enumerar dispositivos MIDI (Interface Digital do Instrumento Musical) e enviar e receber mensagens MIDI de um aplicativo WinUI. O Windows oferece suporte a MIDI via USB (compatível com a classe USB e com a maioria dos drivers proprietários), MIDI via Bluetooth LE e, por meio de soluções de terceiros disponíveis gratuitamente, MIDI via Ethernet e MIDI com roteamento.

Criar uma classe auxiliar para monitoramento de dispositivos

O Windows. Devices.Enumeration namespace fornece o DeviceWatcher que pode notificar seu aplicativo se os dispositivos forem adicionados ou removidos do sistema ou se as informações de um dispositivo forem atualizadas. Como os aplicativos habilitados para MIDI normalmente estão interessados em dispositivos de entrada e saída, este exemplo cria uma classe auxiliar que implementa o padrão DeviceWatcher , para que o mesmo código possa ser usado para dispositivos de entrada MIDI e de saída MIDI, sem a necessidade de duplicação.

Adicione uma nova classe ao seu projeto para servir como seu observador de dispositivo. Neste exemplo, a classe se chama MidiDeviceWatcher. O restante do código nesta seção é usado para implementar a classe auxiliar.

Adicione algumas variáveis de membro à classe:

  • Um objeto DeviceWatcher que monitorará as alterações no dispositivo.
  • Uma string de seletor de dispositivo que conterá, para uma instância, a string do seletor da porta de entrada MIDI e, para outra instância, a string do seletor da porta de saída MIDI.
  • Um controle ListBox que será preenchido com os nomes dos dispositivos disponíveis.
  • Uma DispatcherQueue necessária para atualizar a interface do usuário a partir de uma thread diferente da thread da interface do usuário.
DeviceWatcher deviceWatcher;
string deviceSelectorString;
ListBox deviceListBox;
DispatcherQueue dispatcherQueue;

Adicione uma propriedade DeviceInformationCollection usada para acessar a lista atual de dispositivos de fora da classe auxiliar.

public DeviceInformationCollection? DeviceInformationCollection { get; set; }

No construtor de classe, o chamador passa a cadeia de caracteres do seletor de dispositivo MIDI, a ListBox para listar os dispositivos e o DispatcherQueue necessário para atualizar a interface do usuário.

Chame DeviceInformation.CreateWatcher para criar uma nova instância da classe DeviceWatcher , passando a cadeia de caracteres do seletor de dispositivo MIDI.

Registre manipuladores para os manipuladores de eventos do observador.

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

O DeviceWatcher tem os seguintes eventos:

  • Adicionado - Gerado quando um novo dispositivo é adicionado ao sistema.
  • Removido – Gerado quando um dispositivo é removido do sistema.
  • Atualizado – Gerado quando as informações associadas a um dispositivo existente são atualizadas.
  • EnumerationCompleted – Gerado quando o observador concluir sua enumeração do tipo de dispositivo solicitado.

No manipulador de eventos para cada um desses eventos, um método auxiliar, UpdateDevices, é chamado para atualizar o ListBox com a lista atual de dispositivos. Como o UpdateDevices atualiza os elementos da interface do usuário e esses manipuladores de eventos não são chamados no thread da interface do usuário, cada chamada deve ser encapsulada em uma chamada para 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();
    });
}

O método auxiliar UpdateDevices chama DeviceInformation.FindAllAsync e atualiza o ListBox com os nomes dos dispositivos retornados, conforme descrito anteriormente neste artigo.

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

Adicione métodos para iniciar o observador, usando o método Start do objeto DeviceWatcher e para parar o observador usando o método Stop.

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

Forneça um destruidor para cancelar o registro dos manipuladores de eventos do observador e definir o observador de dispositivo como nulo.

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

Criar portas MIDI para enviar e receber mensagens

No código atrás da janela, declare variáveis de membro para manter duas instâncias da classe auxiliar MidiDeviceWatcher , uma para dispositivos de entrada e outra para dispositivos de saída.

MidiDeviceWatcher? inputDeviceWatcher;
MidiDeviceWatcher? outputDeviceWatcher;

Declare também variáveis de membro para os objetos de porta de entrada e saída MIDI.

MidiInPort? midiInPort;
IMidiOutPort? midiOutPort;

Crie uma nova instância das classes auxiliares do observador, passando a cadeia de caracteres do seletor de dispositivo, a ListBox a ser preenchida e o objeto DispatcherQueue . Em seguida, chame o método para iniciar o DeviceWatcher de cada objeto.

Logo após cada DeviceWatcher ser iniciado, ele terminará de enumerar os dispositivos atuais conectados ao sistema e aumentará seu evento EnumerationCompleted , o que fará com que cada ListBox seja atualizado com os dispositivos MIDI atuais.

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

inputDeviceWatcher.StartWatcher();

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

outputDeviceWatcher.StartWatcher();

Quando o usuário seleciona um item no ListBox de entrada MIDI, o evento SelectionChanged é acionado. No manipulador desse evento, acesse a propriedade DeviceInformationCollection da classe auxiliar para obter a lista atual de dispositivos. Se houver entradas na lista, selecione o objeto DeviceInformation com o índice correspondente ao SelectedIndex do controle ListBox.

Crie o objeto MidiInPort que representa o dispositivo de entrada selecionado chamando MidiInPort.FromIdAsync, passando a propriedade Id do dispositivo selecionado.

Registre um manipulador para o evento MessageReceived , que é gerado sempre que uma mensagem MIDI é recebida por meio do dispositivo especificado.

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 o manipulador MessageReceived é chamado, a mensagem está contida na propriedade Message de MidiMessageReceivedEventArgs. O Type do objeto de mensagem é um valor da enumeração MidiMessageType indicando o tipo de mensagem recebida. Os dados da mensagem dependem do tipo da mensagem. Este exemplo verifica se a mensagem é do tipo Note On e, em caso afirmativo, exibe o canal MIDI, a nota e a velocidade da mensagem.

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

O manipulador SelectionChanged para o dispositivo de saída ListBox funciona da mesma forma que o manipulador para dispositivos de entrada, exceto que nenhum manipulador de eventos está registrado.

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

Depois que o dispositivo de saída for criado, você poderá enviar uma mensagem criando um novo IMidiMessage para o tipo de mensagem que você deseja enviar. Neste exemplo, a mensagem é uma NoteOnMessage. O método SendMessage do objeto IMidiOutPort é chamado para enviar a mensagem.

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

midiOutPort.SendMessage(midiMessageToSend);

Quando o aplicativo estiver fechando, certifique-se de limpar os recursos do aplicativo. Desregistre seus manipuladores de eventos e defina os objetos de porta de entrada e de saída MIDI como nulos. Interrompa os observadores de dispositivo e defina-os como nulos.

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

Usando o sintetizador General MIDI integrado do Windows

Quando você enumera dispositivos MIDI de saída usando a técnica descrita acima, seu aplicativo descobrirá um dispositivo MIDI chamado "Microsoft GS Wavetable Synth". Este é um sintetizador MIDI geral interno que você pode reproduzir em seu aplicativo.

O SDK de extensão UWP para General MIDI ("Microsoft General MIDI DLS for Universal Windows Apps") não está disponível em projetos WinUI 3. A antiga caixa de diálogo Adicionar extensões de referência > era específica do UWP. No entanto, para a maioria dos cenários da área de trabalho, o GS Wavetable Synth funciona sem o SDK de extensão porque os aplicativos da área de trabalho podem acessar diretamente o banco de som do gm.dls sistema. Se você descobrir que a saída MIDI não produz som, verifique se um dispositivo de saída MIDI está selecionado e se a saída de áudio está configurada corretamente.