Making Music with MIDI and C#

In a previous post, I showed you how to construct a wave file manually by hand, using nothing but program code. In this post, I want to show you how to use the other sound generation device that most Windows machines have, the MIDI Sequencer.

Most Windows-based sound cards these days have some kind of wavetable synth on them. This means that, when you play back a MIDI music file, you’re actually just taking some high quality instrument samples that exist in a hidden part of your operating system, and then stringing them together in the right order to represent the notes you want to play, just before sending that large wave onto your normal sound card audio output.

There’s actually a bit more to the process than my description, but for the purposes of this article, it’ll suffice. Some people, however, may have full-size MIDI keyboards or external synthesizers attached to their PC, myself for example (I have a Yamaha PSR270 and a Yamaha MU10 connected to mine).

Once you start connecting external MIDI equipment, you can start doing some rather more interesting things. For example, it’s possible to connect your MIDI keyboard up, then by responding to the key presses on it, make those keys perform functions on your PC.

All of that is quite a complex task. So, for now at least, I’m just going to show you how to make your device play music.

First, however….

Just What Is MIDI?

MIDI stands for “Musical Instrument Digital Interface” and it’s exactly as the name implies; it’s an electronic interface for connecting musical instruments together. As way of an example, I don’t have to include my PC if I don’t want to; I could simply just connect my keyboard to my synth unit and then use the note keys on my keyboard to play notes on the synth unit.

MIDI is a serial interface, and quite a slow one at that (approx. 3000 bytes per second, or 3k). It doesn’t need to be fast, however, because most MIDI messages can easily fit into 3 bytes. The bigger messages that you might send are normally only sent once at start up, so that there’s no slowing down or delays due to large amounts of data.

For you running on Windows, however, you may only ever have been familiar with “MIDI Files.” “MIDI Files” are a file format that holds collections of these MIDI messages, which are then sent to and processed by the instrument in question. What often confuses many developers is just what the difference is between them and, say, a wave or MP3 file.

The best way to describe the difference is to imagine the difference between a sheet of music and a recording of music.

Sheet music (those books with the straight lines and funny circles with sticks them on that musicians are so fond of) are a bit like an instruction manual on how to play that piece of music. It contains no information on what each instrument should sound like, only what note it should play at any given time.

Recorded music, such as you might see in a wave file, is exactly that. It’s a snapshot of someone’s interpretation of the instructions to play a piece of music at the time they played it.

A “MIDI File,” therefore, is a set of instructions, sent to a musical instrument, instructing it as to what the resulting music should sound like.

I could go into a full blown description at this point, but I’m not going to because that’s not what this article is about. If you’re curious, however, the Wikipedia article at

http://en.wikipedia.org/wiki/MIDI

is a good place to start.

Okay, I Understand That, but Why Would I Want to Do This? What’s Wrong with Waves?

There’s nothing wrong with waves, if all you want to do is to play back some pre-recorded audio. With MIDI, however, there’s more to it than just sending music commands.

There’s a large variety of control and display hardware. The MU10 synthesizer I have, for example, has two audio in ports on it. Via these ports, I can attach external sound sources, such as another PC laying audio, microphone, or a CD player. I then can use MIDI commands to control the volume and mixing of these signals. Many rides and attractions in theme parks use MIDI data to control them, and stage shows often use laser and lighting setups that are controlled in the same manner. However, most folks wouldn’t think that things like the instruments that come with games like “Rockband” are also all controlled using MIDI data.

Okay, so enough with the theory. Let’s take a look at some code…

Start a simple command line project in Visual Studio as a code base to work from.

Now comes the hard part.

Under .NET, there are no official assemblies built into the run time that allow you to access the MIDI hardware on your computer. This means that you must use P/Invoke to call through to the original native operating system calls to use them.

Unfortunately, that does mean that it will be harder to port your code to other .NET platforms, and before we can do anything useful we’ll need to create some P/Invoke definitions. I covered P/Invoke in a previous post in this column, so I’m not going to go into any detail about how it all works. To get the definitions you need, add the following to your “Program class” just after the start of the class, but before any other definitions:

[DllImport("winmm.dll")]
private static extern long mciSendString(string command,
   StringBuilder returnValue, int returnLength, IntPtr winHandle);

[DllImport("winmm.dll")]
private static extern int midiOutGetNumDevs();

[DllImport("winmm.dll")]
private static extern int midiOutGetDevCaps(Int32 uDeviceID,
   ref MidiOutCaps lpMidiOutCaps, UInt32 cbMidiOutCaps);

[DllImport("winmm.dll")]
private static extern int midiOutOpen(ref int handle,
   int deviceID, MidiCallBack proc, int instance, int flags);

[DllImport("winmm.dll")]
protected static extern int midiOutShortMsg(int handle,
   int message);

[DllImport("winmm.dll")]
protected static extern int midiOutClose(int handle);

You’ll also need to add a “struct” to your project so that MIDI device information can be obtained.

A struct is very similar to a class that only contains properties, but derives from the native C and C++ languages and so is specially suited to transferring data from managed code to unmanaged code. Don’t worry, though; you don’t have to change much. Create a new class in your project called “MidiOutCaps.cs” and add the following code to it:

using System;
using System.Runtime.InteropServices;

namespace MidiSample
{
   [StructLayout(LayoutKind.Sequential)]
   public struct MidiOutCaps
   {
      public UInt16 wMid;
      public UInt16 wPid;
      public UInt32 vDriverVersion;

      [MarshalAs(UnmanagedType.ByValTStr,
         SizeConst = 32)]
      public String szPname;

      public UInt16 wTechnology;
      public UInt16 wVoices;
      public UInt16 wNotes;
      public UInt16 wChannelMask;
      public UInt32 dwSupport;
   }

}

Remember to change the namespace to match that of your own project’s namespace.

Finally, make sure you have the following using declarations in your main “Program.cs” file:

using System;
using System.Runtime.InteropServices;
using System.Text;

You also might want to add the following just after the P/Invoke definitions:

private delegate void MidiCallBack(int handle, int msg,
   int instance, int param1, int param2);

We won’t be using it in this post, but if you decide to do MIDI input or need the MIDI system to call back to your application with messages and events, you’ll need to define this to allow you to set a callback handler.

At this point, we should now be able to start using the API.

We’ll start with a simple example. If all you want to do is play back a pre-recorded MIDI music file, the simplest thing to do is to use the Windows multimedia control interface, otherwise known as the “MCI.”

You can see in the preceding definitions a definition for “mciSendString.” This is used to send MCI command strings to the MCI subsystem, and is used by sending simple command strings.

You can use “mciSendString” to control many different things, not just playback of MIDI. For our post, though, make sure you have a MIDI file on hand and use the following code to have your PC play this file back, via the default MIDI sequencer device:

var res = String.Empty;

res = Mci("open \"M:\\anger.mid\" alias music");
res = Mci("play music");
Console.ReadLine();    // Pause until return is pressed
res = Mci("close music");

I’ve wrapped each of the above calls in the following function, just to make things easier:

static string Mci(string command)
{
   int returnLength = 256;
   StringBuilder reply = new StringBuilder(returnLength);
   mciSendString(command, reply, returnLength, IntPtr.Zero);
   return reply.ToString();
}

A couple of things you MUST take note of when using the MCI: First off, filenames that include spaces MUST be quoted; if you don’t, the MCI will refuse to load the file; secondly, path names must not have the period character in them.

“c:\my.files\music.midi”

will fail to play, even though it’s quoted, due to the period between “my” and “files”. The golden rule is to use as short and space-free file name as possible and to make sure it’s quoted.

If everything has worked, your program should start playing your chosen MIDI file, and pause for you to press return before stopping and exiting.

There may be times, however, when you want to send individual commands directly to your chosen MIDI device. To start with, let’s find out how many output devices are present in the system:

var numDevs = midiOutGetNumDevs();
Console.WriteLine("You have {0} midi output
   devices", numDevs);

MIDI devices are numbered starting at 0 (zero), so if the above tells you that you have two devices, your MIDI device IDs will be from 0 to 1. If it reports 3, your MIDI devices will be numbered 0, 1, and 2.

To find out the details of a numbered device, you need to use “midiOutGetDevCaps”, as follows:

MidiOutCaps myCaps = new MidiOutCaps();
var res = midiOutGetDevCaps(0, ref myCaps,
   (UInt32)Marshal.SizeOf(myCaps));

The most interesting part of the information returned is the device name available in the “szPname” field in the struct. The other fields can be looked up on MSDN (just search for midioutgetdevcaps); many of the fields have set constants that allow you to determine the type of MIDI technology, supported channels, and other useful information. However, not all drivers and technology support all the different flags.

Once you know the ID of the device you want to open, you then can open it and obtain a handle to it as follows:

int handle = 0;
int deviceNumber = 0;
res = midiOutOpen(ref handle, deviceNumber,
   null, 0, 0);

Upon success, the handle variable will be filled with a large integer number representing the handle allocated. When you’re finished, you must ensure that you close this to free up the MIDI resource, especially if you’ve allocated exclusive use of the device.

At this point, you now can send short MIDI commands to your device. For example, to play a note you would send a note on command:

0x90 = Note on command on channel 0. (The upper 4 bits are the command number, and the lower 4 are the channel; in this case, 0.)

0x3C = The note to play (60 in decimal or a ‘C’ in octave 4 as the following table shows:

Music
Table 1: Decimal values of musical notes

0x7F = The velocity with which to hit the note (127 = maximum volume and force). The lower the number, the softer the sound.

Because the entire message fits into 3 bytes, we can send the entire message in a standard 32-bit integer. However, because of byte order or “Endiness” on a standard PC architecture, we need to reverse the order of the bytes, so in hexadecimal we have to send the following to the MIDI device to send this note on command.

0x007F3C90 (Decimal – 8338576)

As you can see, the command is at the end, with our 3 bytes in reverse order and a 00 to pad things out.

You can combine the values you need by doing the following

byte command = 0x90;
byte note = 0x3C;
byte velocity = 0x7F;int message = (velocity << 16) +
   (note << 8) + command;

Which you then can easily send to your MIDI device by using “midiOutShortMessage”:

var res = midiOutShortMsg(handle, 0x007F3C90);

If everything worked, you should hear your device play a note. If you have no external devices, then because we’ve used device 0, your default will likely be the built-in Windows wavetable synth. Either way, you should hear a note play.

Once you play a note, you should also send a note stop command once it’s played for the length of time you wanted it to. You do this the same way as for playing a note, but change the command byte from 0x90 to 0x80.

You can find a full list of the basic MIDI command messages on the MIDI manufacturers association web page at

http://www.midi.org/techspecs/midimessages.php

If you want to select different instrument sounds on your MIDI device, you’ll need to use the “Program Change” message with the appropriate instrument number. That information can be found here:

http://www.midi.org/techspecs/gm1sound.php

To make your program play your note on an electric guitar, for example, send

0x000019C0

to your MIDI device just before you send the note.

Finally, don’t forget to close the device when you’re finished. The full code for “Program.cs” should look something like this:

using System;
using System.Runtime.InteropServices;
using System.Text;

namespace MidiSample
{
   class Program
   {
      // MCI INterface
      [DllImport("winmm.dll")]
      private static extern long mciSendString(string command,
         StringBuilder returnValue, int returnLength,
         IntPtr winHandle);

      // Midi API
      [DllImport("winmm.dll")]
      private static extern int midiOutGetNumDevs();

      [DllImport("winmm.dll")]
      private static extern int midiOutGetDevCaps(Int32 uDeviceID,
         ref MidiOutCaps lpMidiOutCaps, UInt32 cbMidiOutCaps);

      [DllImport("winmm.dll")]
      private static extern int midiOutOpen(ref int handle,
         int deviceID, MidiCallBack proc, int instance, int flags);

      [DllImport("winmm.dll")]
      private static extern int midiOutShortMsg(int handle,
         int message);

      [DllImport("winmm.dll")]
      private static extern int midiOutClose(int handle);

      private delegate void MidiCallBack(int handle, int msg,
         int instance, int param1, int param2);

      static string Mci(string command)
      {
         StringBuilder reply = new StringBuilder(256);
         mciSendString(command, reply, 256, IntPtr.Zero);
         return reply.ToString();
      }

      static void MciMidiTest()
      {
         var res = String.Empty;

         res = Mci("open \"M:\\anger.mid\" alias music");
         res = Mci("play music");
         Console.ReadLine();
         res = Mci("close crooner");
      }

      static void Main()
      {
         int handle = 0;

         var numDevs = midiOutGetNumDevs();
         MidiOutCaps myCaps = new MidiOutCaps();
         var res = midiOutGetDevCaps(0, ref myCaps,
            (UInt32)Marshal.SizeOf(myCaps));

         res = midiOutOpen(ref handle, 0, null, 0, 0);
         res = midiOutShortMsg(handle, 0x000019C0);
         res = midiOutShortMsg(handle, 0x007F3C90);
         res = midiOutClose(handle);

      }

   }
}

As you can see, direct MIDI programming is not for the faint hearted, and it requires a LOT of work to implement correctly. You’ll need to write file parsing routines, you’ll need to study the MIDI implementation charts for the device you’re working with, you’ll need to build timing routines and command sequencing code, and all that’s before you even get anywhere near building a user interface.

It’s for this reason I suggest you invest some time in looking at the excellent “NAudio” (naudio.codeplex.com) sound library that contains many common MIDI classes and managed APIs to make your job easier. With NAudio you can, if you want, still get the low-level individual messages control as seen in this post if you want, or you can just use its higher level functionality to set up and run your own MIDI data sequencers and file parsers.

Got a burning question about .NET? or just want to know how to make A do B? Come and hunt me down on the interweb; you can normally find me in the Lidnug .NET user group on the Linked-in platform, or you can find me on Twitter as @shawty_ds. Let me know your thoughts or simply just leave a comment below.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read