Veni, MIDI, Vici: Generating a simple MIDI file using VB, part 1 (Matt Gertz)

Anthony D. Green [MSFT]

As I’ve alluded to in previous blogs, music has always been a big part of my life, particularly performance music.  I’ve been a clarinet and saxophone player for many years, am an avid singer, and (with the help of friends) I’ve done my best to teach myself piano and drums. 

Composition of music, however, has always been a problem for me.  I’ve written a few small pieces, but any attempt to write something larger has always ended up with me giving up after fifty or so measures.  Programs to help a person create their own compositions have certainly come a long way, but they all have their idiosyncrasies.  For example, one aspect of this that I’ve never been able to wrap my head around are MIDI channels and instruments – invariably, I always get these set wrong as my brain seems incapable of grokking the nuances of those areas.  I’m still waiting for a low-cost composition program in which I can say “Piano plays this, clarinet plays this, and bass plays that,” and ignore the details of channels and whatnot.

It was simpler (though far less powerful!) when I had a Commodore 128.  This was a gift I’d received for graduation, a fun machine that had three different operating systems (C64, C128, and CP/M) built into it.  In the C128-mode, the version of BASIC included in the OS had a “Play” command which took a string of characters representing note pitches and durations.  There were three voices which you could set to different instruments (well, envelopes, really), and the Play() call was asynchronous, so you could make chords by coding three Play() commands in a row.  In practice, there was still a certain lag between the voices – it was not a speedy machine – but it was still a lot of fun to set up music in it.

I’ve decided that I’d enjoy the composition process more, given the tools that I have, if I understood how MIDI actually worked.  After searching around on the Internet, I found a good article at Skytopia (http://www.skytopia.com/project/articles/midi.html) which listed out the basics of the MIDI format and also provided “for further information” links.  Given that, I set out to create a MIDI-generation program which would mimic my C-128 experience, as well as give me some grounding in the MIDI format.

Caveat:  I’m sure many readers will be far more knowledgeable about MIDI than I am, and will justifiably roll their eyes at this code due to its limited scope.  I will only be generating simple type-1 MIDI files that only use the Note On and Note Off events, and which don’t use metadata, targeting a “Save” scenario only.  The idea of this exercise was to understand MIDI and how to code against it in VB, not to generate a fully-functional music generation program.  Coding up “Load” and supporting more of the MIDI functionality would be a more time-consuming process which maybe I’ll get to one day.

I started out by creating a Windows Application called VBMidi.  But before I coded up the form, I took some time to craft out some support code to help me, as covered in the next section.

The gist of MIDI

MIDI files are composed of a well-defined header containing general information about the music, followed by a series of tracks containing actual notes, durations, and volumes for each track.  To capture this, I added a new class to my application called MIDI.  (Right-click the project, choose “Add,” and then choose “Class…”)

In this class, I set up some of the boilerplate information:

Public Class MIDI

    ‘ These are fixed data:

    Dim MIDIHeader() As Byte = {&H4D, &H54, &H68, &H64, &H0, &H0, &H0, &H6}

    Dim SubFormatType() As Byte = {&H0, &H1} ‘ Type-1 MIDI file (as opposed to Type-0)

 

MIDIHeader is always the first thing in the MIDI file, and just identifies this as a MIDI file.  SubFormatType, on the other hand, is a two-byte field which, in my case, contains the bytes to identify this as a type-1 MIDI file (i.e., supporting multiple tracks, unlike type-0).  Note that I’m using hexadecimal to define the bytes.  When working with byte information, it’s traditional to use hex instead of decimal, as it’s a lot easier to see what will happen when bytes are concatenated or split apart into “nibbles” (groups of 4 bits).

The speed of the music is something I’ll also want to track at a high level, and I define that here:

    Const ticksPerBeat = &H80

    ‘ These could be changed (in theory) by the program

    Dim Speed() As Byte = {&H0, ticksPerBeat } ‘ Default to 128 ticks per beat

 

Speed can support two bytes, but as I’m defaulting to 128 (= &H80) ticks per beat, the first byte is 0.

All of the other data is owned by the tracks.  I opted to create a nested class called “Track” to hold it.  One of the first things I did in Track was to define the events that could exist:

    Public Class Track

 

        Public Enum NoteEvent

            NoteOff = &H8

            NoteOn = &H9

 

            ‘ Advanced

            AfterTouch = &HA

            ControlChange = &HB

            ProgramChange = &HC

            ChannelPressure = &HD

            PitchWheel = &HE

        End Enum

In this example, I’ll only be using the first two events – turning a note on, and turning it off.  Each Track also has header and exit information:

        ‘ These are fixed data

        Dim TrackHeader() As Byte = {&H4D, &H54, &H72, &H6B}

        Dim TrackOut() As Byte = {&H0, &HFF, &H2F, &H0}

 

And then there’s all of the stuff in-between, such as the actual note data (stored in bytes) and metadata (also stored in bytes).  I’ll be defining a container for metadata for the sake of completeness, even though I won’t be using it in this example:

        ‘ These can be changed by the program

        Public TrackData As New List(Of Byte)

        Dim TrackMetadata As New List(Of Byte)

 

Music can be played on one of 16 channels (&H0 through &HF).  I’ve opted to create a “one channel per track” rule in my code, although this isn’t a requirement in the MIDI format.  I’ve used channel = -1 to indicate that the track isn’t being used (even using a signed byte, I still have space for this, since the channel inform ation only takes half of a byte), and I created a function to tell me if that’s the case:

        Public Channel As SByte = -1

        Public Function ValidTrack() As Boolean

            Return Channel >= 0

        End Function

 

Now, I need a way to actually add a note to the track.  That function looks like this:

        Public Sub AddNoteOnOffEvent(ByVal beatOffset As Double, ByVal ev As NoteEvent, _

ByVal note As Byte, ByVal volume As Byte)

 

This method requires a bit of explanation. First, let’s cover the arguments passed in: 

·         The caller will pass in the note’s beat offset (the difference between this event and the previous one)  via a Double — so, for example, assuming we’re in 4/4 time with a quarter note taking one beat, an eighth-note offset would be passed in as 0.5. 

·         The NoteEvent determines whether I’m turning on or off a note (there are other event possibilities, but as I’ve mentioned before, I’m going to ignore them for this application). 

·         The Note is just an index into the list of possible tones (for example, middle C is a 60 = &H3C).

·          Finally, volume is just a value between 0 and 127.

 Now, into the code for the function. If the track currently has no channel set for it, then I don’t bother adding the note and instead just return, since the note information has to be combined with the channel information. 

            If Not ValidTrack() Then Return

 

The next thing I do in that code is translate the note’s beatOffset to something MIDI can understand.  The duration is with respect to the ticks per beat (defined earlier in the constant called rate as being 128 ticks per beat), so I multiply the two numbers together to get the number of ticks, casting it as an unsigned integer into something called tickOffset.

            Dim tickOffset As UInt32 = CType(beatOffset * rate, UInt32)

 

Now I can actually start adding bytes to the TrackData.  Assuming that the NoteEvent is either an On or Off, I’ll first add the tickOffset into the data.  However, I can’t just blast in the value of tickOffset.  MIDI requires a special format for durations which is a little screwy.  Instead of saying “the duration data lasts this many bytes, and here they are,” they instead require a format where the final byte in the duration sequence is less than 128 (i.e., less than &H80).  So, 127 ticks would be formatted as &H7F, but 128 ticks would be formatted as &H80 &H00, and this increases up to maximum possible value of &HFF &HFF &HFF &H7F, representing 268435455 ticks.  I have a helper function to create this formatting for me, which I found at http://jedi.ks.uiuc.edu/~johns/links/music/midifile.htm and which I translated to VB:

 

        Private Function TranslateTickTime(ByVal ticks As UInt32) As Byte()

            Dim value As UInt32 = ticks

            Dim buffer As UInt32

            buffer = ticks And &H7F

            value = value >> 7

            While value > 0

                buffer = buffer << 8

                buffer = buffer

1 comment

Leave a comment

  • Jozef Hanzalik 0

    Good day,I apologize, I do not know English, so I write via translator.My name is Jozef, I come from the Slovak Republic.According to your instructions I have created application and MIDI files. It works perfectly, I just needed to change the piano instrument to Harmonica and add four channels.I can not do it.  Can you help me?Please rewrite the new code?
    Thank you very much. Jozef

Feedback usabilla icon