Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Handling Device add/remove in Universal Apps (MIDI in this case)

Pete Brown - 29 December 2014

Many device-oriented apps (bluetooth, MIDI, etc.) require you to restart the app when you want to detect a new device. I've seen this both in Windows Store/Universal apps, and even in big desktop apps. Instead, these apps should detect changes during runtime, and respond gracefully.

Example:

You load up a synthesizer app. After it loads, you realize you forgot to plug in your keyboard controller. You then plug in the controller, but the app doesn't do anything.

Why does that happen? It happens because the app enumerates the devices at startup, but doesn't register for change notifications using the DeviceWatcher. The app likely enumerates devices using code similar to this:

var selector = MidiInPort.GetDeviceSelector();

var devices = await DeviceInformation.FindAllAsync(selector);

if (devices != null && devices.Count > 0)
{
    // MIDI devices returned
    foreach (var device in devices)
    {
        list.Items.Add(device);
    }
}

That's a nice and simply approach to listing devices, but it doesn't allow for change notification.

The rest of this post will show how to enumerate devices (using MIDI devices and the preview WinRT MIDI API on NuGet) and register to receive change notifications. The project will eventually be a test project for my Novation Launchpads, but for this post, we'll focus only on the specific problem of enumeration and change notification.

A custom device metadata class

First, rather than use the DeviceInformation class directly, I've used my own metadata class. This provides more flexibility for the future by decreasing reliance on the built-in class. It's also lighter weight, holding only the information we care about.

namespace LaunchpadTest.Devices
{
    class MidiDeviceInformation
    {
        public string Id { get; set; }
        public string Name { get; set; }
        public bool IsDefault { get; set; }
        public bool IsEnabled { get; set; }
    }
}

With that out of the way, let's look at the real code.

The MIDI class

Next, we'll create a class for interfacing with the MIDI device information in Windows. I'll list the entire source here and refer to it in the description that follows

using System;
using System.Collections.Generic;
using System.Linq;
using WindowsPreview.Devices.Midi;
using Windows.Devices.Enumeration;

namespace LaunchpadTest.Devices
{
    class Midi
    {
        public List<MidiDeviceInformation> ConnectedInputDevices { get; private set; }
        public List<MidiDeviceInformation> ConnectedOutputDevices { get; private set; }

        private DeviceWatcher _inputWatcher;
        private DeviceWatcher _outputWatcher;

        public event EventHandler InputDevicesEnumerated;
        public event EventHandler OutputDevicesEnumerated;

        public event EventHandler InputDevicesChanged;
        public event EventHandler OutputDevicesChanged;

        // using an Initialize method here instead of the constructor in order to
        // prevent a race condition between wiring up the event handlers and
        // finishing enumeration
        public void Initialize()
        {
            ConnectedInputDevices = new List<MidiDeviceInformation>();
            ConnectedOutputDevices = new List<MidiDeviceInformation>();

            // set up watchers so we know when input devices are added or removed
            _inputWatcher = DeviceInformation.CreateWatcher(MidiInPort.GetDeviceSelector());

            _inputWatcher.EnumerationCompleted += InputWatcher_EnumerationCompleted;
            _inputWatcher.Updated += InputWatcher_Updated;
            _inputWatcher.Removed += InputWatcher_Removed;
            _inputWatcher.Added += InputWatcher_Added;

            _inputWatcher.Start();

            // set up watcher so we know when output devices are added or removed
            _outputWatcher = DeviceInformation.CreateWatcher(MidiOutPort.GetDeviceSelector());

            _outputWatcher.EnumerationCompleted += OutputWatcher_EnumerationCompleted;
            _outputWatcher.Updated += OutputWatcher_Updated;
            _outputWatcher.Removed += OutputWatcher_Removed;
            _outputWatcher.Added += OutputWatcher_Added;

            _outputWatcher.Start();
        }


        private void OutputWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
        {
            // let other classes know enumeration is complete
            if (OutputDevicesEnumerated != null)
                OutputDevicesEnumerated(this, new EventArgs());
        }

        private void OutputWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
        {
            // this is where you capture changes to a specific ID
            // you could change this to be more specific and pass the changed ID
            if (OutputDevicesChanged != null)
                OutputDevicesChanged(this, new EventArgs());
        }

        private void OutputWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
        {
            // remove from our collection the item with the specified ID

            var id = args.Id;

            var toRemove = (from MidiDeviceInformation mdi in ConnectedOutputDevices
                            where mdi.Id == id
                            select mdi).FirstOrDefault();

            if (toRemove != null)
            {
                ConnectedOutputDevices.Remove(toRemove);

                // notify clients
                if (OutputDevicesChanged != null)
                    OutputDevicesChanged(this, new EventArgs());
            }
        }

        private void OutputWatcher_Added(DeviceWatcher sender, DeviceInformation args)
        {
            var id = args.Id;

            // you could use DeviceInformation directly here, using the
            // CreateFromIdAsync method. However, that is an async method
            // and so adds a bit of delay. I'm using a trimmed down object
            // to hold MIDI information rather than using the DeviceInformation class

#if DEBUG
            // this is so you can see what the properties contain
            foreach (var p in args.Properties.Keys)
            {
                System.Diagnostics.Debug.WriteLine("Output: " + args.Name + " : " + p + " : " + args.Properties[p]);
            }
#endif   

            var info = new MidiDeviceInformation();
            info.Id = id;
            info.Name = args.Name;
            info.IsDefault = args.IsDefault;
            info.IsEnabled = args.IsEnabled;

            ConnectedOutputDevices.Add(info);

            // notify clients
            if (OutputDevicesChanged != null)
                OutputDevicesChanged(this, new EventArgs());
        }


        // Input devices =============================================================

        private void InputWatcher_EnumerationCompleted(DeviceWatcher sender, object args)
        {
            // let other classes know enumeration is complete
            if (InputDevicesEnumerated != null)
                InputDevicesEnumerated(this, new EventArgs());
        }

        private async void InputWatcher_Updated(DeviceWatcher sender, DeviceInformationUpdate args)
        {
            // this is where you capture changes to a specific ID
            // you could change this to be more specific and pass the changed ID
            if (InputDevicesChanged != null)
                InputDevicesChanged(this, new EventArgs());
        }

        private void InputWatcher_Removed(DeviceWatcher sender, DeviceInformationUpdate args)
        {
            // remove from our collection the item with the specified ID

            var id = args.Id;

            var toRemove = (from MidiDeviceInformation mdi in ConnectedInputDevices
                            where mdi.Id == id
                            select mdi).FirstOrDefault();

            if (toRemove != null)
            {
                ConnectedInputDevices.Remove(toRemove);

                // notify clients
                if (InputDevicesChanged != null)
                    InputDevicesChanged(this, new EventArgs());
            }
        }

        private void InputWatcher_Added(DeviceWatcher sender, DeviceInformation args)
        {
            var id = args.Id;

            // you could use DeviceInformation directly here, using the
            // CreateFromIdAsync method. However, that is an async method
            // and so adds a bit of delay. I'm using a trimmed down object
            // to hold MIDI information rather than using the DeviceInformation class

#if DEBUG
            // this is so you can see what the properties contain
            foreach (var p in args.Properties.Keys)
            {
                System.Diagnostics.Debug.WriteLine("Input: " + args.Name + " : " + p + " : " + args.Properties[p]);
            }
#endif            

            var info = new MidiDeviceInformation();
            info.Id = id;
            info.Name = args.Name;
            info.IsDefault = args.IsDefault;
            info.IsEnabled = args.IsEnabled;

            ConnectedInputDevices.Add(info);

            // notify clients
            if (InputDevicesChanged != null)
                InputDevicesChanged(this, new EventArgs());
        }
    }
}

 

There's some debug information in that class. You may find, when enumerating devices, that the properties contain information that will be useful to your app. So, I've added code to enumerate those properties and spit them out to the debug window. This code takes time, though, so make sure you don't include it in a production app.

The downloadable version of this class also implements IDisposable to unhook the events, and eventually dispose of other resources. This is a standard pattern implemented with code generated by Visual Studio 2015.

The DeviceWatcher class

The DeviceWatcher is the heart of this class. This is a Windows Runtime class that lets a developer listen for changes for any type of device in the system that they can normally find or enumerate using the DeviceInformation functions.

The developer simply creates a DeviceWatcher using the device selector for the devices they are interested in. A device selector can be thought of like a query or filter; it's used to filter the full device list down to only the ones you're interested in. Most device interfaces provide a way to easily get the selector for those devices. For example, MidiOutPort and MidiInPort both expose GetDeviceSelector methods which return the filter/query string.

_inputWatcher = DeviceInformation.CreateWatcher(MidiInPort.GetDeviceSelector());
_outputWatcher = DeviceInformation.CreateWatcher(MidiOutPort.GetDeviceSelector());

 

Once the watcher is created, the app should wire up the appropriate events and then start the watcher.

_outputWatcher.EnumerationCompleted += OutputWatcher_EnumerationCompleted;
_outputWatcher.Updated += OutputWatcher_Updated;
_outputWatcher.Removed += OutputWatcher_Removed;
_outputWatcher.Added += OutputWatcher_Added;

_outputWatcher.Start();

The events are important. The EnumerationCompleted event tells your app that all of the appropriate devices have had Added events fired. Typically you'd use this to then load the list into your UI, or notify the app to do so.

The Updated event tells your app that metadata about a device has been updated. For MIDI, this is typically not useful. Some other devices may use this, however.

The Added and Removed events tell the app when a device has been added or removed from the system. This is the most important part when it comes to change notifications. These are the two events that most apps do not pay attention to, and so require restarting to pick up device changes.

The Start method starts the watcher. Ensure you have wired up your events before calling this.

 

The test app

I built a little XAML/C# Universal app to test this out, using Visual Studio 2015. I unloaded the phone project as the preview MIDI API is available only for Windows (and only for 64 bit if you're on a 64 bit machine, or 32 bit if you're on a 32 bit machine -- "any CPU" is not supported for the preview.)

Here's a cropped version of the app on my PC

image

The UI is pretty simple. I have two list box controls which show the MIDI device names, IDs, whether they are enabled and whether or not they are the default device.

A note on MIDI device names

You may have noticed a few things.

1. The MIDI device names shown are not super helpful.

2. Not all MIDI devices on my system showed up in the list. You can see, for example, I have no default MIDI device listed.

We're working on both of those. The latter is being tracked as a bug for certain MOTU and Roland devices; our enumeration code missed those devices because of how they show up in the device tree. If you've used the preview MIDI API and have had one or more devices fail to enumerate, please let us know as soon as possible. We're testing a lot of devices, but I want to make sure we have this right when we launch Windows 10, and more data is better.

As for names, we're working on a proper scheme so the names are meaningful and consistent. Obviously blank names are not helpful. In my case, it's some of my Novation products (my two Launchpad S controllers in particular) that are coming up with blank names.

XAML:

The XAML is just two list box controls with a heading.

<Page
    x:Class="LaunchpadTest.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:LaunchpadTest"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">
    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.Resources>
            <Style TargetType="TextBlock" x:Key="TextBlockStyle">
                <Setter Property="FontSize" Value="20" />
                <Setter Property="Margin" Value="5" />
            </Style>
            <Style TargetType="TextBlock" x:Key="HeaderTextBlockStyle">
                <Setter Property="FontSize" Value="26" />
                <Setter Property="Margin" Value="10,20,10,20" />
            </Style>
        </Grid.Resources>                                
        <Grid Margin="50">
            <Grid.RowDefinitions>
                <RowDefinition Height="*" />
                <RowDefinition Height="*" />
            </Grid.RowDefinitions>

            <StackPanel Grid.Row="0" Margin="5">
                <TextBlock Text="Input Devices" Style="{StaticResource HeaderTextBlockStyle}"/>
                <StackPanel Orientation="Horizontal" Margin="10">
                    <TextBlock Text="Name" Width="240" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Enabled" Width="90" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Default" Width="90" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Id" Style="{StaticResource TextBlockStyle}" />
                </StackPanel>
                
                <ListBox x:Name="InputDevices">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="250" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="*" />
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding Name}" Grid.Column="0" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding IsEnabled}" Grid.Column="1" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding IsDefault}" Grid.Column="2" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding Id}" Grid.Column="3" Style="{StaticResource TextBlockStyle}"/>
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </StackPanel>
            
            <StackPanel Grid.Row="1" Margin="5">
                <TextBlock Text="Output Devices" Style="{StaticResource HeaderTextBlockStyle}"/>
                <StackPanel Orientation="Horizontal" Margin="10">
                    <TextBlock Text="Name" Width="240" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Enabled" Width="90" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Default" Width="90" Style="{StaticResource TextBlockStyle}" />
                    <TextBlock Text="Id" Style="{StaticResource TextBlockStyle}" />
                </StackPanel>
                <ListBox x:Name="OutputDevices">
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="250" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="100" />
                                    <ColumnDefinition Width="*" />
                                </Grid.ColumnDefinitions>
                                <TextBlock Text="{Binding Name}" Grid.Column="0" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding IsEnabled}" Grid.Column="1" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding IsDefault}" Grid.Column="2" Style="{StaticResource TextBlockStyle}"/>
                                <TextBlock Text="{Binding Id}" Grid.Column="3" Style="{StaticResource TextBlockStyle}"/>
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </StackPanel>
        </Grid>
        
    </Grid>
</Page>

x

Code-behind:

For purposes of this test, I put all the code in the main page's code-behind. The code here is responsible for creating the Midi class and listening to the events it fires off for device enumeration and device change.

using LaunchpadTest.Devices;
using System;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace LaunchpadTest
{
    public sealed partial class MainPage : Page
    {
        private Midi _midi;

        public MainPage()
        {
            Loaded += MainPage_Loaded;

            this.InitializeComponent();
        }

        private void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            _midi = new Midi();

            _midi.OutputDevicesEnumerated += _midi_OutputDevicesEnumerated;
            _midi.InputDevicesEnumerated += _midi_InputDevicesEnumerated;

            _midi.Initialize();
        }

        private async void _midi_InputDevicesEnumerated(object sender, EventArgs e)
        {
            if (!Dispatcher.HasThreadAccess)
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    InputDevices.ItemsSource = _midi.ConnectedInputDevices;

                    // only wire up device changed event after enumeration has completed
                    _midi.InputDevicesChanged += _midi_InputDevicesChanged;

                });
            }
        }

        private async void _midi_InputDevicesChanged(object sender, EventArgs e)
        {
            if (!Dispatcher.HasThreadAccess)
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    InputDevices.ItemsSource = null;
                    InputDevices.ItemsSource = _midi.ConnectedInputDevices;
                });
            }
        }

        // Output devices ------------------------------------------------

        private async void _midi_OutputDevicesEnumerated(object sender, EventArgs e)
        {
            if (!Dispatcher.HasThreadAccess)
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, ()=>
                {
                    OutputDevices.ItemsSource = _midi.ConnectedOutputDevices;

                    // only wire up device changed event after enumeration has completed
                    _midi.OutputDevicesChanged += _midi_OutputDevicesChanged;
                });
            }
        }

        private async void _midi_OutputDevicesChanged(object sender, EventArgs e)
        {
            if (!Dispatcher.HasThreadAccess)
            {
                await Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
                {
                    OutputDevices.ItemsSource = null;
                    OutputDevices.ItemsSource = _midi.ConnectedOutputDevices;
                });
            }
        }
    }
}

In the code-behind, you can see how I respond to the device change events by rebinding the data to the list box controls.

Also, you'll see that I do the initial binding only once I receive notification that the devices have been enumerated. At that time, I also wire up the device changed events. If you wire them up earlier, you'll get a bunch of events during enumeration, and that will be just noise for your app.

Run the app with a USB MIDI interface plugged in. Then, after the interface shows up in the list, unplug it from Windows. You should see it disappear from the list box(es).

Summary

I like to work with MIDI and synthesizers, but this approach will work for any of the WinRT recognized devices your apps can use. It's a good practice to respond properly to device add and removal to either show new devices, or ensure you stop communicating with disconnected ones.

This pattern wasn't exactly obvious from the materials we've supplied on MSDN, so I hope this post has helped clear up the usage of the DeviceWatcher.

Now go build some device apps, especially MIDI. :)

References

       
posted by Pete Brown on Monday, December 29, 2014
filed under:        

6 comments for “Handling Device add/remove in Universal Apps (MIDI in this case)”

  1. Terjesays:
    Thank you very much for sharing this. I'm also really exited about the new Win RT midi api. This is exactly what I need to be able to make the app I have in my mind. Really looking forward to dive into this new api. Thanks for the effort you are putting into this!
  2. Yannicksays:
    Hi,

    i'm working to detect my Yamaha Motif ES Rack but i'dont get in the names of Input or output midi port the name Yamaha !

    Is it normal ?

    Thanks

Comment on this Post

Remember me