Overview
The average user only has one MIDI playback device on their system, the internal software synthesizer 'Microsoft GS Wavetable Synth', and any MIDI files are played back via that - no problem. However, many users (including me and probably you as you're reading this) have superior or multiple devices, either software or hardware based, so we want/need to be able to select the playback device that the system uses.
Pre Vista, you could select the playback device via Sounds and Audio Devices|Audio in the Control Panel. For some reason, Microsoft decided to remove this option in Vista, and it hasn't reappeared in any service pack to date or in Windows 7 (Beta) either, even though there have been many requests for its inclusion.
In February 2008, I made a tiny application to address this which worked fine on my Vista systems and I made publicly available elsewhere. This didn't work for everybody though. Some people didn't get any devices listed, others only a selection of the ones available. I'm currently in the process of writing a comprehensive managed wrapper around all things MIDI, and it occurred to me that I should revisit the problem and solve it conclusively as part of that project. This article is the result.
The problem with my previous solution was that it used the Registry to retrieve information about installed devices. Not all devices create these Registry entries, hence they weren't all shown by the application.
The Solution
Getting the Devices
To retrieve all the MIDI out devices turns out to be trivial - although it requires the use of a little PInvoke, calling a function in winmm.dll: midiOutGetNumDevs
. This is a very simple function. It takes no parameters, and simply returns the number of output devices.
[DllImport("winmm.dll")]
static extern UInt32 midiOutGetNumDevs();
We could then simply use any device ID, from 0 to the result of this function -1 (it's zero based) and save this, but that's not very user friendly. Nobody knows the ID number of their devices, only the name, so we need another function to get information about each device including the name, midiOutGetDevCaps
. This function is slightly more complicated.
[DllImport("winmm.dll")]
static extern UInt32 midiOutGetDevCaps(Int32 uDeviceID,
ref MIDIOUTCAPS lpMidiOutCaps, UInt32 cbMidiOutCaps);
The first parameter is the ID of the device we want to query. Device IDs are sequential and zero based, so we can simply use a foreach
loop based on the result we retrieved above and recursively call this function.
The second parameter is a structure MIDIOUTCAPS
where the function will store the device information.
[StructLayout(LayoutKind.Sequential)]
struct MIDIOUTCAPS
{
public UInt16 wMid;
public UInt16 wPid;
public UInt32 vDriverVersion;
[MarshalAs(UnmanagedType.ByValTStr,
SizeConst = Constants.MAXPNAMELEN)]
public string szPname;
public UInt16 wTechnology;
public UInt16 wVoices;
public UInt16 wNotes;
public UInt16 wChannelMask;
public UInt32 dwSupport;
}
It's only the szPname
that we're interested in. The constant MAXPNAMELEN
is from the MMSystem.h file (I've not included it in the project as most of it is not needed), and has a value of 32.
The third parameter is the size of the MIDIOUTCAPS
structure. The function returns a value that indicates the error (if any) that occurred when the function was called. We're only interested if there was no error (when it returns MMSYSERR_NOERROR
, this constant's value is 0
).
Output class
To call this function for each ID from the managed world, we need a class Output
that takes a MIDIOUTCAPS
in the constructor.
public class Output
{
public Output(Int32 id, MIDIOUTCAPS caps)
{
ID = id;
Name = caps.szPname;
}
public Int32 ID
{
get;
private set;
}
public string Name
{
get;
private set;
}
}
Now, we can call the midiOutGetDevCaps
recursively and return a read only list.
public static void Load()
{
outputs = null;
List<output> devices = new List<output>();
UInt32 numberOfDevices = Functions.midiOutGetNumDevs();
if (numberOfDevices > 0)
{
for (Int32 i = 0; i < numberOfDevices; i++)
{
MIDIOUTCAPS caps = new MIDIOUTCAPS();
if (Functions.midiOutGetDevCaps(i, ref caps,
(UInt32)Marshal.SizeOf(caps)) == Constants.MMSYSERR_NOERROR)
{
devices.Add(new Output(i, caps));
}
}
}
outputs = devices.AsReadOnly();
}
Saving the ID
Windows uses a Registry setting that contains the ID of the default MIDI out device. The key is: HKEY_CURRENT_USER\Software\Microsoft\ActiveMovie\devenum\{4EFE2452-168A-11D1-BC76-00C04FB9453B}\Default MidiOut Device, and the value that needs changing is a DWORD
called MidiOutId
.
All we need to do is change this value to the ID of our preferred device and we're done.
RegistryKey defaultKey = null;
try
{
defaultKey = Registry.CurrentUser.OpenSubKey(DefaultMidiOutDevice,
RegistryKeyPermissionCheck.ReadWriteSubTree,
RegistryRights.SetValue);
defaultKey.SetValue(MidiOutId, value);
}
finally
{
if (defaultKey != null)
{
defaultKey.Close();
}
}
Conclusion
Unless Microsoft decides to make breaking changes in later OSes to this part of the Registry or to winmm.dll, this should work under all future versions too. I've tested it on XP (not needed, but it works anyway!), Vista, and Windows 7 Beta (build 7000).
I've included the binary (as well as the source) for anyone that just needs the application to take back control of their MIDI system.
References
History
- 10th May 2009: Initial version