MIDI Programming - A Complete Study
Part 2 - Data Structures

Written by Stéphane Richard (Mystikshadows)

INTRODUCTION:

Welcome to the second part of the MIDI Programming Series. As I mentionned in the first part, we'll see one way of loading up a MIDI file in memory (in structures) so we can manipulate them at will. Although when we send this MIDI information to the MIDI ports will have to take the different pieces of information of each MIDI event and put them back into a series of bytes, we'll see here how useful it can be to treat these informations as independant fields. Now since we won't be playing the song per se, I'll be implementing the functions so that the give back the information in a printed textual fashion so you can see we actually did read the MIDI file.

There's alot of coding information up ahead in this part of the series, we'll see exactly what information we can actually get from a MIDI file's header and track information for one thing. Likewise, we'll see exactly what needs to be done with each type of MIDI events. Some of these Events require 2 parameters to be read while others require only one parameter. So, how do we deal with all these different types of MIDI related information? We'll see that right here, right now. So fasten your safety belts as we implement a complete MIDI file loader and information extractor utility to give us everything that can be extracted from the MIDI file.

OPENING AND LOADING A MIDI FILE:

When you open a MIDI, it's not enough to open it and load it into, say a string variable, and think you're ready to play it. At least not at the level we'll be doing it here. You need something that can isolate the tracks and the isolate the midi events themselves. But first, let's declare some constants that will need when reading the midi file. Help make the code clearer in the process.

' ---------------------- ' Constant Definitions ' ---------------------- ' ------------------- ' MIDI File Formats ' ------------------- CONST SingleTrack = 0 CONST MultipleTracksSync = 1 CONST MultipleTracksAsync = 2 ' ---------------------------------- ' MIDI Status Constant Definitions ' ---------------------------------- CONST NoteOff = &H80 CONST NoteOn = &H90 CONST PolyKeypress = &HA0 CONST ControllerChange = &HB0 CONST ProgramChange = &HC0 CONST ChannelPressure = &HD0 CONST PitchBend = &HE0 CONST SysExMessage = &HF0 ' ----------------------------------- ' MIDI Controller Numbers Constants ' ----------------------------------- CONST MolulationWheel = 1 CONST BreathController = 2 CONST FootController = 4 CONST PortamentoTime = 5 CONST MainVolume = 7 CONST Balance = 8 CONST Pan = 10 CONST ExpressController = 11 CONST DamperPedal = 64 CONST Portamento = 65 CONST Sostenuto = 66 CONST SoftPedal = 67 CONST Hold2 = 69 CONST ExternalFXDepth = 91 CONST TremeloDepth = 92 CONST ChorusDepth = 93 CONST DetuneDepth = 94 CONST PhaserDepth = 95 CONST DataIncrement = 96 CONST DataDecrement = 97

Now when we read a MIDI event, we'll need somekind of structure to hold the information that is found in the MIDI event itself. As I explained in the first part of the series, a regular MIDI. To this effect, take a look at the following two user defined types:

TYPE TimeStructure Bar AS INTEGER Beat AS INTEGER Tick AS INTEGER END TYPE TYPE MIDIEventStructure EventTime AS TimeStructure EventDeltaTime AS BYTE StatusByte AS BYTE ChannelByte AS BYTE DataByteOne AS BYTE DataByteTwo AS BYTE END TYPE

Now we need another structure to encompass the MIDIEventStructure into an array. we'll need one array per track. For the sake of simplicity of this example, I won't use a dynamic array in each track so we won't be needing pointers and linked list but typically that is what you would use in a case like this since you don't know how long a song (or each of it's composing tracks) are. Here's the TrackStructure:

TYPE TrackStructure TrackNumber AS INTEGER TrackEventCount AS INTEGER MIDIEvents(131072) AS MIDIEventStructure ' 128 Kb / Track END TYPE

These three structures is what we're going to use to keep the song in memory. The TimeStructure holds the standard beat:bar:tick information. The MIDIEventStructure holds the EventTime (a TimeStructure sub type) and the Event information. The EventDeltaTime tells the system to send the MIDI event right now but not play the event until DeltaTime has been reached. In MIDI it's a good technique to use to make sure the data that is to be played is already sent through the MIDI port and ready to play when the time is right. The StatusByte is the actual MIDI Event Type (refer to the first part of the series for a list of these MIDI Event Types). The ChannelByte tells which MIDI channel the MIDI event is to be sent through. DataByteOne and DataByteTwo form the parameters to the current MIDI event itself.

Because the StatusByte and the ChannelByte are essentially the MSB (Most Significant Byte) and LSB (Least Significant Byte) of a one byte value we will need the following 4 support functions in order to get the MSB and LSB from the byte and store them in the StatusByte and ChannelByte fields of our MIDIEventStructure. The last function allows to get the length of a variable length MIDI message (such as SysEx).

' ========================================= ' This function returns the LSB of a byte ' Example: LowNibbble(&HD5) = &H5 ' ========================================= FUNCTION LowNibble(ByteValue AS BYTE) AS BYTE LowNibble = ByteValue AND &H0F END FUNCTION ' ========================================= ' This function returns the LSB of a byte ' Example: HighNibble(&HD5) = &HD0 ' ========================================= FUNCTION HighNibble(ByteValue AS BYTE) AS BYTE HighNibble = ByteValue AND &HF0 END FUNCTION ' ========================================= ' This function calculates the length of ' a variable length MIDI message. ' ========================================= FUNCTION GetVariableLength( MidiHandle AS INTEGER, _ Position AS INTEGER) AS INTEGER DIM Value AS INTEGER DIM Character AS BYTE Get #ch, Pos, C: Pos = Pos + 1 Value = C If (Value And &H80) <> 0 Then Value = Value And &H7F Do Value = Value * 128 Get #ch, Pos, C: Pos = Pos + 1 C = C And &H7F Value = Value + C Loop While (C And &H80) <> 0 End If readVarLen = Value End Function ' =============================================== ' Takes a byte (0-255) and returns a HEX string ' =============================================== FUNCTION HexByte(WorkByte AS BYTE) AS STRING DIM WorkText AS STRING WorkText = Hex(WorkByte) If Len(WorkText) = 1 THEN WorkText = "0" + WorkText HexByte = WorkText END FUNCTION ' ================================================ ' If NoteNumber is valid note, Creates Note Name ' and it's octave and returns that in a string ' ================================================ FUNCTION IsNote(ByVal NoteNumber As Long) As String DIM Octave AS INTEGER DIM WorkNote AS INTEGER DIM Note AS STRING Octave = NoteNumber \ 12 WorkNote = NoteNumber Mod 12 SELECT CASE WorkNote + 1 CASE 1 Note = "C" CASE 2 Note = "C#" CASE 3 Note = "D" CASE 4 Note = "D#" CASE 5 Note = "E" CASE 6 Note = "F" CASE 7 Note = "F#" CASE 8 Note = "G" CASE 9 Note = "G#" CASE 10 Note = "A" CASE 11 Note = "A#" CASE 12 Note = "B" END SELECT Note = Note + TRIM$(STR$(Octave)) IsNote = Note END FUNCTION

With all these tools, we're ready to start the loading process. First thing will do is get some variables ready to read information from the file. We'll open that file in binary mode since we'll be reading bytes from it, binary is the only way to open the file in this case. Note that This is my version of some code I grabbed off the Internet, I altered it considerably for the sake of clarity of code and I commented it to help you better understand. I can say however, that if I would have created it totally it would have probably looked like this so I saw fit to just alter this code.

' ---------------------------- ' Work Variable Declarations ' ---------------------------- DIM MIDIString AS STRING DIM SongTracks() AS TrackStructure ' Tracks of a MIDI file

Now that we have our variables declared, we can go ahead and open the MIDI file in question. I have include the song 1492 a piece from the Christopher Columbus movie for our needs. Now we will be implementing 2 subroutines here. The first is the MIDIFileFormatProperties() function and the other is the Actual ReadMIDIFile Function itself. Since ReadMIDIFile makes use of the ReadMIDIFileFormatProperties function we'll implement that last one first.

' ======================================================= ' NAME: ReadMIDIFileFormatProperties() ' PARAMETERS: MidiHandle AS INTEGER ' Position AS INTEGER ' EndOfTrack AS INTEGER ' ASSUMES: MidiHandle links to already Opened File ' RETURNS: A String containing MIDI File Properties ' CALLED FROM: The ReadMIDIFile() Function ' ------------------------------------------------------- ' DESCRIPTION: This function takes the pass MIDI file ' handle and current position in the file ' and gets information from the file that ' it adds to a string. When it is done, ' it will return that string to the ' calling function (ReadMIDIFile in this ' case). ' ======================================================= FUNCTION MIDIFileFormatProperties( BYREF MidiHandle AS INTEGER, _ BYVAL Position AS INTEGER, _ BYREF EndOfTrack AS INTEGER ) AS STRING ' ---------------- ' Work Variables ' ---------------- DIM Counter AS LONG DIM Bytes AS LONG DIM DataByte AS BYTE DIM DataByte2 AS BYTE DIM DataByte3 AS BYTE DIM DataByte4 AS BYTE DIM DataByte5 AS BYTE DIM WorkString AS STRING DIM WorkItem AS STRING * 13 ' ----------------------------------------- ' We Get the next byte from the MIDI file ' ----------------------------------------- GET #MidiHandle, Position, DataByte2 Position = Position + 1 ' ------------------------------------------------------ ' IF the byte read is 0 we can get the sequence number ' ------------------------------------------------------ IF DataByte2 = 0 THEN GET #MidiHandle, Position, DataByte3 Position = Position + 1 IF DataByte3 = 0 Then WorkString = WorkString + "seqnr/posfile" ELSE GET #MidiHandle, Position, DataByte4 Position = Position + 1 GET #MidiHandle, Position, DataByte5 Position = Position + 1 WorkString = WorkString + "Sequence Number " + _ CStr(B5 * 256 + B4) END IF ' ----------------------------------------------------- ' If the byte read is between 1 and 7 inclusively, we ' can get some more specific information. ' ----------------------------------------------------- ELSEIF DataByte2 >= 1 AND DataByte2 <= 7 THEN SELECT CASE DataByte2 CASE 1 WorkItem = "text" + " - " CASE 2 WorkItem = "copyright" + " - " CASE 3 WorkItem = "seq/tr. name" + " - " CASE 4 WorkItem = "instrument" + " - " CASE 5 WorkItem = "lyric" + " - " CASE 6 WorkItem = "marker" + " - " CASE 7 WorkItem = "cue point" + " - " END SELECT WorkString = WorkString & WorkItem Bytes = GetVariableLength(MidiHandle, Position) FOR Counter = 1 To Bytes Get #MidiHandle, Position, DataByte Position = Position + 1 WorkString = Workstring + CHR$(DataByte) NEXT Counter ' ------------------------------------------------------ ' IF the byte read is &H20 we can get the MIDI Channel ' ------------------------------------------------------ ELSEIF DataByte2 = &H20 Then WorkString = WorkString + "midi channel " Get #MidiHandle, Position, DataByte3 Position = Position + 1 Get #MidiHandle, Position, DataByte4 Position = Position + 1 If DataByte3 <> 0 Then WorkString = WorkString + "Length Error" WorkString = WorkString + HexByte(DataByte4) ' ---------------------------------------------------------- ' IF the byte read is &H21 we can get the MIDI Port Number ' ---------------------------------------------------------- ELSEIF DataByte2 = &H21 Then WorkString = WorkString + "midi port " Get #MidiHandle, Position, DataByte3 Position = Position + 1 Get #MidiHandle, Position, DataByte4 Position = Position + 1 If DataByte3 <> 0 THEN WorkString = WorkString + "Length Error" WorkString = WorkString + HexByte(DataByte4) ' ------------------------------------------------------------- ' A byte value of &H21 indicates the End of the current track ' ------------------------------------------------------------- ElseIf DataByte2 = &H2F Then WorkString = WorkString + "end of track " Get #MidiHandle, Position, DataByte3 Position = Position + 1 EndOfTrack = True ' ----------------------------------------------------------- ' A byte value of &H51 Means we're reading the song's tempo ' ----------------------------------------------------------- ELSEIF DataByte2 = &H51 THEN WorkString = WorkString + "tempo " GET #MidiHandle, Position, DataByte3 Position = Position + 1 Bytes = DataByte3 If Bytes <> 3 Then WorkString = WorkString + "Length Error" GET #MidiHandle, Position, DataByte3 Position = Position + 1 GET #MidiHandle, Position, DataByte4 Position = Position + 1 GET #MidiHandle, Position, DataByte5 Position = Position + 1 WorkString = WorkString + _ CStr(CLng(60000000 / CLng(CLng(DataByte3) * _ 256 * 256 + CLng(DataByte4) * 256 + _ CLng(DataByte5)))) + " BPM" ' ----------------------------------------------------- ' A byte value of &H54 Gets SMPTE Offsets Information ' ----------------------------------------------------- ELSEIF DataByte2 = &H54 THEN WorkString = WorkString + "SMPTE Offs " GET #MidiHandle, Position, DataByte3 Position = Position + 1 Bytes = DataByte3 IF Bytes <> 5 THEN WorkString = WorkString + "Length Error" FOR Counter = 1 TO Bytes Get #MidiHandle, Position, DataByte Position = Position + 1 WorkString = WorkString + HexByte(DataByte) NEXT Counter ' ---------------------------------------------------------- ' A byte value of &H58 Indicates the song's time signature ' ---------------------------------------------------------- ELSEIF DataByte2 = &H58 THEN WorkString = WorkString + "time sign " GET #MidiHandle, Position, DataByte3 Position = Position + 1 Bytes = DataByte3 IF Bytes <> 4 THEN WorkString = WorkString + "Length Error" GET #MidiHandle, Position, DataByte4 Position = Position + 1 GEt #MidiHandle, Position, DataByte5 Position = Position + 1 WorkString = WorkString + CStr(DataByte4) + "/" + _ CStr(2 ^ DataByte5) + " - " GET #MidiHandle, Position, DataByte4 Position = Position + 1 WorkString = WorkString + DataByte4 + " clocks/metr.click - " GET #MidiHandle, Position, DataByte5 Position = Position + 1 WorkString = WorkString + DataByte5 + " 32nd/quarter " ' --------------------------------------------------------- ' A byte value of &H59 Indicates the song's key signature ' --------------------------------------------------------- ELSEIF DataByte2 = &H59 THEN WorkString = WorkString + "key sign " GET #MidiHandle, Position, DataByte3 Position = Position + 1 Bytes = DataByte3 IF Bytes <> 2 THEN WorkString = WorkString + "Length Error" FOR Counter = 1 TO Bytes GET #MidiHandle, Position, DataByte Pos = Pos + 1 WorkString = WorkString + HexByte(DataByte) & " " NEXT Counter ' ----------------------------------------------------------- ' A byte value of &H7F Indicates a variable length property ' ----------------------------------------------------------- ELSEIF DataByte2 = &H7F Then Bytes = GetVariableLength(MidiHandle, Position) WorkString = WorkString + "propr.- len " + CStr(Bytes) Position = Position + Bytes END IF ' --------------------------------------------------------------- ' Finally we return the formulated string to the calling module ' --------------------------------------------------------------- MIDIFileFormatProperties = WorkString END FUNCTION

Quite a function wouldn't you agree? This particular function will be executed in the Track Loop of the ReadMIDIFile() function. Hence it will be able to get this information, if available, for all tracks of a song. Now comes the core function itself. Let us now take a look at the ReadMIDIFile Function itself.

' ======================================================= ' NAME: ReadMIDIFile() ' PARAMETERS: File AS STRING ' ASSUMES: File is a valid MIDI file name ' RETURNS: A String containing MIDI File Properties ' CALLED FROM: The main part of the program. ' ------------------------------------------------------- ' DESCRIPTION: this function reads and displays the ' contents of a MIDI file. ' ======================================================= Function ReadMIDIFile(File AS STRING) AS STRING ' ---------------- ' Work Variables ' ---------------- DIM FileHandle AS INTEGER DIM Counter AS INTEGER DIM EventCounter AS INTEGER DIM WorkString AS STRING DIM DeltaTimeString AS STRING * 7 DIM StatusLabel AS STRING DIM SectionLabel AS STRING * 4 DIM EventLabel AS STRING DIM FormatType AS INTEGER DIM NumberOfTracks AS INTEGER DIM Track AS INTEGER DIM Resolution AS INTEGER DIM BeatsPerMinute AS INTEGER DIM NumberOfBytes AS INTEGER DIM ByteRead AS INTEGER DIM StringBytes AS INTEGER DIM Status AS BYTE DIM Position AS INTEGER DIM PreviousPosition AS INTEGER DIM TempPosition AS INTEGER DIM MessageLength AS INTEGER DIM DataByte AS BYTE DIM DataByte1 AS BYTE DIM DataByte2 AS BYTE DIM DataByte3 AS BYTE DIM DataByte4 AS BYTE DIM DataByte5 AS BYTE DIM DeltaTime AS INTEGER DIM CurrentTrack AS INTEGER DIM EndOfTrack AS INTEGER WorkString = WorkString & UCASE$(File)) + CHR$(13) ' ----------------------------- ' First we open the MIDI File ' ----------------------------- FileHandle = FREEFILE OPEN File FOR BINARY AS #FileHandle ' --------------------------- ' We read the first 4 bytes ' --------------------------- GET #FileHandle, 1, SectionLabel IF SectionLabel <> "MThd" THEN WorkString = "Invalid MIDI File Header" GOTO ReadMidiFileEND END IF ' -------------------------- ' We read the next 4 bytes ' -------------------------- GET #FileHandle, 5, DataByte1 GET #FileHandle, 6, DataByte2 GET #FileHandle, 7, DataByte3 GET #FileHandle, 8, DataByte4 IF NOT (DataByte1 = 0 AND DataByte2 = 0 AND DataByte3 = 0 AND DataByte4 = 6) THEN WorkString = WorkString + "Midi Header length is wrong." GOTO ReadMidiFileEND END IF ' ------------------------------------- ' Read the File Format and display it ' ------------------------------------- Get #FileHandle, 9, DataByte1 Get #FileHandle, 10, DataByte2 FormatType = DataByte1 * 256 + DataByte2 WorkString = WorkString & "Format type = " + CStr(FormatType) SELECT CASE FormatType CASE 0 WorkString = WorkString + " - single track any channel" + CHR$(13) CASE 1 WorkString = WorkString + " - multi tracks sep channels" + CHR$(13) CASE 2 WorkString = WorkString + " - multi patterns-songs" + CHR$(13) CASE ELSE WorkString = WorkString + " - Invalid MIDI File Format" GoTo ReadMidiFileEND END SELECT ' -------------------------------------------- ' Read the Number Of Tracks and Display Them ' -------------------------------------------- Get #FileHandle, 11, DataByte1 Get #FileHandle, 12, DataByte2 NumberOfTracks = DataByte1 * 256 + DataByte2 WorkString = WorkString + "Number of Tracks = " & CStr(NumberOfTracks) + CHR$(13) If FormatType = 0 AND NumberOfTracks > 1 THEN WorkString = WorkString + "Invalid Number of tracks for file format" GOTO ReadMidiFileEND END IF ' --------------------------------------------- ' We REDIM the Tracks Array to NumberOfTracks ' --------------------------------------------- REDIM SongTracks(1 To NumberOfTracks) AS TrackStructure ' -------------------------------------------- ' Read the Resolution in PPQN And Display it ' -------------------------------------------- Get #FileHandle, 13, DataByte1 Get #FileHandle, 14, DataByte2 Resolution = DataByte1 * 256 + DataByte2 WorkString = WorkString + "Resolution = " + CStr(Resolution) + " PPQN" + CHR$(13) ' ------------------------------------------------------- ' Offset Position Number to 15 and start loading tracks ' ------------------------------------------------------- Position = 15 FOR Track = 1 TO NumberOfTracks EndOfTrack = 0 SongTracks(Track).TrackNumber = Track GET #FileHandle, Position, SectionLabel IF SectionLabel <> "MTrk" THEN WorkString = WorkString + "Invalid Track Header Section" GOTO ReadMidiFileEND END IF GET #FileHandle, , DataByte1 GET #FileHandle, , DataByte2 GET #FileHandle, , DataByte3 GET #FileHandle, , DataByte4 NumberOfBytes = CLng(CLng(DataByte1) * 256 ^ 3 + _ CLng(DataByte2) * 256 ^ 2 + _ CLng(DataByte3) * 256 + CLng(DataByte4)) WorkString = WorkString + CHR$(13) + "Track " + CStr(Track) + _ " length = " + CStr(NumberBytes) + CHR$(13) Position = Position + 8 PreviousPosition = Position Status = 0 ' ---------------------------------------------- ' This WHILE Loop gets the data from the track ' ---------------------------------------------- WHILE Position - PreviousPosition < NumberOfBytes GET #FileHandle, Position, DataByte1 If DataByte1 = &HFF Then Position = Position + 1 Status = B1 EventLabel = MIDIFileFormatProperties(FileHandle, Position, EndOfTrack) Stat = " " & HexByte(Status) & " " ELSE DeltaTime = GetVariableLength(FileHandle, Position) DeltaTimeString = CStr(DeltaTime) GET #FileHandle, Position, DataByte1 If (DataByte1 And &H80) = &H80 Then Status = DataByte1 StatusLabel = " " & HexByte(Status) & " " Position = Position + 1 Else StatusLabel = "r" & HexByte(Status) & " " End If SongTracks(Track).MIDIEvents(EventCounter).DeltaTime = DeltaTime SongTracks(Track).MIDIEvents(EventCounter).StatusByte = HighNibble(Status) SongTracks(Track).MIDIEvents(EventCounter).ChannelByte = LowNibble(Status) ' --------------------------------------- ' This Select Case loads the parameters ' based on the MIDI Event Type. ' --------------------------------------- SELECT CASE HighNibble(Status) Case NoteOff Get #FileHandle, Position, DataByte2 Position = Position + 1 Get #FileHandle, Position, DataByte3 Position = Position + 1 EventLabel = "Note off.... " + isNote(DataByte2) + "-" + CStr(DataByte3) Case NoteOn Get #FileHandle, Position, DataByte2 Position = Position + 1 Get #FileHandle, Position, DataByte3 Position = Position + 1 EventLabel = "Note on..... " + isNote(DataByte2) + "-" + CStr(DataByte3) CASE PolyKeypress Get #FileHandle, Position, DataByte2 Position = Position + 1 Get #FileHandle, Position, DataByte3 Position = Position + 1 EventLabel = "Poly Keypress... " + CStr(DataByte2) + "-" + CStr(DataByte3) Case ControllerChange Get #FileHandle, Position, DataByte2 Position = Position + 1 Get #FileHandle, Position, DataByte3 Position = Position + 1 EventLabel = "Ctl Change.. " + HexByte(DataByte2) + " " + HexByte(DataByte3) Case ProgramChange Get #FileHandle, Position, DataByte2 Position = Position + 1 EventLabel = "Prg Change.. " + HexByte(B2) Case ChannelPressure Get #FileHandle, Position, DataByte2 Position = Position + 1 EventLabel = "Chan Press.. " + HexByte(B2) Case PitchBend Get #FileHandle, Position, DataByte2 Position = Position + 1 Get #FileHandle, Position, DataByte3 Position = Position + 1 EventLabel = "Pitch bend.. " + HexByte(DataByte2) + " " + HexByte(DataByte3) Case SysExMessage Select Case Status Case &HFE Case &HFF EventLabel = MIDIFileFormatProperties(FileHandle, Position, EndOfTrack) Case &HF0 TempPosition = Position MessageLength = GetVariableLength(FileHandle, Position) EventLabel = "SysEx - len: " & CStr(MessageLength) Position = Position + MessageLength Case &HF7 Case Else End Select End Select ' --------------------------------------------------- ' We Populate our Structure with It's Proper Values ' --------------------------------------------------- SongTracks(Track).MIDIEvents(EventCounter).DataByteOne = DataByte2 SongTracks(Track).MIDIEvents(EventCounter).DataByteTwo = DataByte3 End If SongTracks(Track).TrackEventCount = SongTracks(Track).TrackEventCount + 1 IF EventLabel <> "" THEN WorkString = WorkString & deltaT & Stat & reg + CHR$(13) EventLabel = "" END IF EventCounter = EventCounter + 1 WEND NEXT Track ' -------------------------------------------------------- ' Close The File Handle and return The Formulated String ' -------------------------------------------------------- ReadMidiFileEND: CLOSE #FileHandle readMidiFile = WorkString END FUNCTION

There you have it This last function does the actual opening of the MIDI file and drives the loading of the MIDI events into our Structure. Although these are long functions. I think it's fairly clear to see what's going in here. Note that the EventLabel that gets populated in each of the Event Types is there for informational purposes only. When we actually do something with the MIDI File (in the next part of the series) the parts of this code that create a string for display will be taken off.

The main part of the program would simply need to call the ReadMIDIFile with the file 1492.mid as it's parameter for the whole system to load the file into our SongTracks MIDIEvents array of structures. Such as:

' ------------------------------------------------------------ ' Call The Read MIDI File to load the song in memory ' and report some information about the song and it's tracks ' ------------------------------------------------------------ MIDIString = ReadMIDIFile("1492.mid")

IN CONCLUSION:

And there you have it, as promised, we now have a system that can effectively load a file into a structure that can be manipulated as you would any other array. The basic reason why SysEx information is treated so differently is simply because it is not a fixed piece of information. Anything can happen in SysEx (anything that is allowed by the MIDI gear itself that is). The basic idea is to load up the SysEx information in a string and send that information right to the MIDI port and MIDI channel you want. The MIDI Gear will receive this information and if it can (if one piece of equipment connected to the MIDI network can process the SysEx, it will). Other than that, it's a bunch of parameters to standard MIDI event types.

In the next part of the series, we'll implement the playback engine itself. This is where timing will take it's importance as we will have a program that will load the MIDI file and actually play it in realtime. With that part, all of what you've learned in the past 2 parts of the series will really fit together as you'll see how everything is connected together to form a complete MIDI player. OF course, if you have questions or comments, you know how to reach me. So by all means please do so so we can make sure everyone is up to par on what I just did in this part of the series. It will be important to understand what's happening here before we go to the next part of the series. Until then, Happy MIDI programming. MystikShadows
Stéphane Richard
mystikshadows@gmail.com