## 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