═══ 1. Preface ═══ MIDI files are a very popular format for storing "musical performances" and other related data. Most professional music programs that deal with such data use this standardized format. Although the description of this file format is available from the MIDI Manufacturers Association, I believe that a more explicit and "down to earth" explanation (as well as a nicely organized, online OS/2 book) would be appreciated by programmers who want to adapt their software to read and write MIDI files. This manual describes the internals of the MIDI file format. Furthermore, although there are some code examples of reading and writing MIDI files, I perceived a need for an OS/2 Dynamic Link library that handles all of the generic "bookkeeping" required to read and write MIDI files. Since there are certain things that all programs must do when reading or writing a MIDI file (ie, open it, read in or write out headers with a ChunkSize, read and write variable length quantities, etc), it makes sense to have the code to do these things shared by all applications. A Dynamic Link Library (ie, DLL) makes this possible under OS/2. DLL's can simplify application development too since a programmer need not compile and link this generic MIDI file reading/writing code for each program. For this reason, I created such a DLL. It was designed to be flexible enough to accomodate a variety of needs and programming algorithms. The second half of this manual describes the DLL, and the accompanying C examples demonstrate how to use it in applications. It's common practice (although not mandated by the format itself) to name MIDI files with a .mid extension. But, since the MIDI file format is an explicitly defined format, the surest way to tell that you're dealing with a MIDI file is to look at the data inside of it for the mandatory chunk headers. Some of the words in this manual are highlighted in colored text, such as Channel Pressure. These are words that refer to types of MIDI messages (ie, streams of bytes that are defined by the MIDI spec itself). Of course, MIDI messages (ie, data bytes) are usually found inside of a MIDI file. Other words are in italics such as Format. These refer to data bytes inside of a MIDI file that aren't defined by the MIDI spec itself, but are nevertheless found inside of a MIDI file because they are important to a sequencer program. Underlined words, such as GM Sound Module, refer to hardware. Of course, audio hardware is used to "play" the music data stored in a MIDI file, but the MIDI file itself isn't tied to any particular piece of hardware. (ie, Most of the data in a MIDI file could be played on an analog synth with oscillators just as easily as a sample-playing computer sound card, although there is a provision to store data for specific units). Words that are in colored text such as Read This are meant to be emphasized, or are headings in this manual. This manual was written by Jeff Glatt. ═══ 2. The MIDI File Format ═══ The Standard MIDI File (SMF) is a file format specifically designed to store the data that a sequencer creates (whether that sequencer be software or hardware based). Note: For an explanation of what a sequencer is, read my other article on such. Essentially, a sequencer is a software program or hardware unit that "plays" a musical performance complete with appropriate rhythmic and melodic inflections (ie, plays music in the context of a musical beat). This format stores the standard MIDI events (ie, status bytes with appropriate data bytes) plus a time-stamp for each event (ie, a series of bytes that represent how many clock pulses to wait before "playing" the event). The format allows saving information about tempo, pulses per quarter note resolution (or resolution expressed in divisions per second, ie SMPTE setting), time and key signatures, and names of tracks and patterns. It can store multiple patterns and tracks so that any application can preserve these structures when loading the file. Note: A track usually is analogous to one musical part, such as a Trumpet part. A pattern would be analogous to all of the musical parts (ie, Trumpet, Drums, Piano, etc) for a song, or excerpt of a song. The format was designed to be generic so that any sequencer could read or write such a file without losing the most important data, and flexible enough for a particular application to store its own proprietary, "extra" data in such a way that another application won't be confused when loading the file and can safely ignore this extra stuff that it doesn't need. Think of the MIDI file format as a musical version of an ASCII text file (except that the MIDI file contains binary data too), and the various sequencer programs as text editors all capable of reading that file. But, unlike ASCII, MIDI file format saves data in chunks (ie, groups of bytes preceded by an ID and size) which can be parsed, loaded, skipped, etc. Therefore, it can be easily extended to include a program's proprietary info. For example, maybe a program wants to save a "flag byte" that indicates whether the user has turned on an audible metronome click. The program can put this flag byte into a MIDI file in such a way that another application can skip this byte without having to understand what that byte is for. In the future, the MIDI file format can also be extended to include new "official" chunks that all sequencer programs may elect to load and use. This can be done without making old data files obsolete (ie, the format is designed to be extensible in a backwardly compatible way). ═══ 2.1. What's a Chunk? ═══ Data is always saved within a chunk. There can be many chunks inside of a MIDI file. Each chunk can be a different size (ie, where size refers to how many bytes are contained in the chunk). The data bytes in a chunk are related in some way. A chunk is simply a group of related bytes. Each chunk begins with a 4 character (ie, 4 ascii bytes) ID which tells what "type" of chunk this is. The next 4 bytes (all bytes are comprised of 8 bits) form a 32-bit length (ie, size) of the chunk. All chunks must begin with these two fields (ie, 8 bytes), which are referred to as the chunk header. Note: The Length does not include the 8 byte chunk header. It simply tells you how many bytes of data are in the chunk following this header. Here's an example header (with bytes expressed in hex): 4D 74 68 64 00 00 00 06 Note that the first 4 bytes make up the ascii ID of Mthd (ie, the first four bytes are the ascii values for 'M', 't', 'h', and 'd'). The next 4 bytes tell us that there should be 6 more data bytes in the chunk (and after that we should find the next chunk header or the end of the file). In fact, all MIDI files begin with this Mthd header (and that's how you know that it's a MIDI file). Note: The 4 bytes that make up the Length are stored in Motorola 68000 byte order, not Intel reverse byte order (ie, the 06 is the fourth byte instead of the first of the four). All multiple byte fields in a MIDI file follow this standard, often called "Big Endian" form. ═══ 2.2. MThd Chunk ═══ The MThd header has an ID of MThd, and a Length of 6. Let's examine the 6 data bytes (which follow the above, 8 byte header) in an MThd chunk. The first two data bytes tell the Format (which I prefer to call Type). There are actually 3 different types (ie, formats) of MIDI files. A type of 0 means that the file contains one single track containing midi data on possibly all 16 midi channels. If your sequencer sorts/stores all of its midi data in one single block of memory with the data in the order that it's "played", then it should read/write this type. A type of 1 means that the file contains one or more simultaneous (ie, all start from an assumed time of 0) tracks, perhaps each on a single midi channel. Together, all of these tracks are considered one sequence or pattern. If your sequencer separates its midi data (i.e. tracks) into different blocks of memory but plays them back simultaneously (ie, as one "pattern"), it will read/write this type. A type of 2 means that the file contains one or more sequentially independant single-track patterns. If your sequencer separates its midi data into different blocks of memory, but plays only one block at a time (ie, each block is considered a different "excerpt" or "song"), then it will read/write this type. The next 2 bytes tell how many tracks are stored in the file, NumTracks. Of course, for format type 0, this is always 1. For the other 2 types, there can be numerous tracks. The last two bytes indicate how many pulses (i.e. clocks) per quarter note resolution the time-stamps are based upon, Division. For example, if your sequencer has 96 ppqn, this field would be (in hex): 00 60 Alternately, if the first byte of Division is negative, then this represents the division of a second that the time-stamps are based upon. The first byte will be -24, -25, -29, or -30, corresponding to the 4 SMPTE standards representing frames per second. The second byte (a positive number) is the resolution within a frame (ie, subframe). Typical values may be 4 (MIDI Time Code), 8, 10, 80 (SMPTE bit resolution), or 100. Note: You can specify millisecond-based timing by the data bytes of -25 and 40 subframes. Here's an example of a complete MThd chunk (with header): 4D 54 68 64 MThd ID 00 00 00 06 Length of the MThd chunk is always 6. 00 01 The Format type is 1. 00 02 There are 2 MTrk chunks in this file. E7 28 Each increment of delta-time represents a millisecond. ═══ 2.3. MTrk Chunk ═══ After the MThd chunk, you should find an MTrk chunk, as this is the only other currently defined chunk. (If you find some other chunk ID, it must be proprietary to some other program, so skip it by ignoring the following data bytes indicated by the chunk's Length.) An MTrk chunk contains all of the midi data (with timing bytes), plus optional non-midi data for one track. Obviously, you should encounter as many MTrk chunks in the file as the MThd chunk's NumTracks field indicated. The MTrk header begins with the ID of MTrk, followed by the Length (ie, number of data bytes to read for this track). The Length will likely be different for each track. (After all, a track containing the violin part for a Bach concerto will likely contain more data than a track containing a simple 2 bar drum beat). ═══ 2.3.1. Variable Length Quantities -- Event's Time ═══ Think of a track in the MIDI file in the same way that you normally think of a track in a sequencer. A sequencer track contains a series of events. For example, the first event in the track may be to sound a middle C note. The second event may be to sound the E above middle C. These two events may both happen at the same time. The third event may be to release the middle C note. This event may happen a few musical beats after the first two events (ie, the middle C note is held down for a few musical beats). Each event has a "time" when it must occur, and the events are arranged within a "chunk" of memory in the order that they occur. In a MIDI file, an event's "time" precedes the data bytes that make up that event itself. In other words, the bytes that make up the event's time-stamp come first. A given event's time-stamp is referenced from the previous event. For example, if the first event occurs 4 clocks after the start of play, then its "delta-time" is 04. If the next event occurs simultaneously with that first event, its time is 00. So, a delta-time is the duration (in clocks) between an event and the preceding event. Note: Since all tracks start with an assumed time of 0, the first event's delta-time is referenced from 0 A delta-time is stored as a series of bytes which is called a variable length quantity. Only the first 7 bits of each byte is significant (right-justified; sort of like an ASCII byte). So, if you have a 32-bit delta-time, you have to unpack it into a series of 7-bit bytes (ie, as if you were going to transmit it over midi in a SYSEX message). Of course, you will have a variable number of bytes depending upon your delta-time. To indicate which is the last byte of the series, you leave bit #7 clear. In all of the preceding bytes, you set bit #7. So, if a delta-time is between 0-127, it can be represented as one byte. The largest delta-time allowed is 0FFFFFFF, which translates to 4 bytes variable length. Here are examples of delta-times as 32-bit values, and the variable length quantities that they translate to: NUMBER VARIABLE QUANTITY 00000000 00 00000040 40 0000007F 7F 00000080 81 00 00002000 C0 00 00003FFF FF 7F 00004000 81 80 00 00100000 C0 80 00 001FFFFF FF FF 7F 00200000 81 80 80 00 08000000 C0 80 80 00 0FFFFFFF FF FF FF 7F Here's some C routines to read and write variable length quantities such as delta-times. With WriteVarLen(), you pass a 32-bit value (ie, unsigned long) and it spits out the correct series of bytes to a file. ReadVarLen() reads a series of bytes from a file until it reaches the last byte of a variable length quantity, and returns a 32-bit value. void WriteVarLen(register unsigned long value) { register unsigned long buffer; buffer = value & 0x7F; while ( (value >>= 7) ) { buffer <<= 8; buffer |= ((value & 0x7F) | 0x80); } while (TRUE) { putc(buffer,outfile); if (buffer & 0x80) buffer >>= 8; else break; } } doubleword ReadVarLen() { register doubleword value; register byte c; if ( (value = getc(infile)) & 0x80 ) { value &= 0x7F; do { value - (value << 7) + ((c = getc(infile)) & 0x7F); } while (c & 0x80); } return(value); } Note: The concept of variable length quantities (ie, breaking up a large value into a series of bytes) is used with other fields in a MIDI file besides delta-times, as you'll see later. ═══ 2.3.2. Events ═══ The first (1 to 4) byte(s) in an MTrk will be the first event's delta-time as a variable length quantity. The next data byte is actually the first byte of that event itself. I'll refer to this as the event's Status. For MIDI events, this will be the actual MIDI Status byte (or the first midi data byte if running status). For example, if the byte is hex 90, then this event is a Note-On upon midi channel 0. If for example, the byte was hex 23, you'd have to recall the previous event's status (ie, midi running status). Obviously, the first MIDI event in the MTrk must have a status byte. After a midi status byte comes its 1 or 2 data bytes (depending upon the status - some MIDI messages only have 1 subsequent data byte). After that you'll find the next event's delta time (as a variable quantity) and start the process of reading that next event. SYSEX (system exclusive) events (status = F0) are a special case because a SYSEX event can be any length. After the F0 status (which is always stored -- no running status here), you'll find yet another series of variable length bytes. Combine them with ReadVarLen() and you'll come up with a 32-bit value that tells you how many more bytes follow which make up this SYSEX event. This length doesn't include the F0 status. For example, consider the following SYSEX MIDI message: F0 7F 7F 04 01 7F 7F F7 This would be stored in a MIDI file as the following series of bytes (minus the delta-time bytes which would precede it): F0 06 7F 04 01 7F 7F F7 Some midi units send a system exclusive message as a series of small "packets" (with a time delay inbetween transmission of each packet). The first packet begins with an F0, but it doesn't end with an F7. The subsequent packets don't start with an F0 nor end with F7. The last packet doesn't start with an F0, but does end with the F7. So, between the first packet's opening F0 and the last packet's closing F7, there's 1 SYSEX message there. (Note: only extremely poor designs, such as the crap marketed by Casio exhibit these aberrations). Of course, since a delay is needed inbetween each packet, you need to store each packet as a separate event with its own time in the MTrk. Also, you need some way of knowing which events shouldn't begin with an F0 (ie, all of them except the first packet). So, the MIDI file spec redefines a midi status of F7 (normally used as an end mark for SYSEX packets) as a way to indicate an event that doesn't begin with F0. If such an event follows an F0 event, then it's assumed that the F7 event is the second "packet" of a series. In this context, it's referred to as a SYSEX CONTINUATION event. Just like the F0 type of event, it has a variable length followed by data bytes. On the other hand, the F7 event could be used to store MIDI REALTIME or MIDI COMMON messages. In this case, after the variable length bytes, you should expect to find a MIDI Status byte of F1, F2, F3, F6, F8, FA, FB, FC, or FE. (Note that you wouldn't find any such bytes inside of a SYSEX CONTINUATION event). When used in this manner, the F7 event is referred to as an ESCAPED event. A status of FF is reserved to indicate a special non-MIDI event. (Note that FF is used in MIDI to mean "reset", so it wouldn't be all that useful to store in a data file. Therefore, the MIDI file spec arbitrarily redefines the use of this status). After the FF status byte is another byte that tells you what Type of non-MIDI event it is. It's sort of like a second status byte. Then after this byte is another byte(s -- a variable length quantity again) that tells how many more data bytes follow in this event (ie, its Length). This Length doesn't include the FF, Type byte, nor the Length byte. These special, non-MIDI events are called Meta-Events, and most are optional unless otherwise noted. What follows are some defined Meta-Events (including the FF Status and Length). Note that unless otherwise mentioned, more than one of these events can be placed in an Mtrk (even the same Meta-Event) at any delta-time. (Just like all midi events, Meta-Events have a delta-time from the previous event regardless of what type of event that may be. So, you can freely intermix MIDI and Meta events). ═══ 2.3.2.1. Sequence Number ═══ FF 00 02 ss ss This optional event which must occur at the beginning of a MTrk (ie, before any non-zero delta-times and before any midi events) specifies the number of a sequence. The two data bytes ss ss, are that number which corresponds to the MIDI Cue message. In a format 2 MIDI file, this number identifies each "pattern" (ie, Mtrk) so that a "song" sequence can use the MIDI Cue message to refer to patterns. If the ss ss numbers are omitted (ie, Length byte = 0 instead of 2), then the MTrk's location in the file is used (ie, the first MTrk chunk is the first pattern). In format 0 or 1, which contain only one "pattern" (even though format 2 contains several MTrks), this event is placed in only the first MTrk. So, a group of format 1 files with different sequence numbers can comprise a "song collection". There can be only one of these events per MTrk chunk. ═══ 2.3.2.2. Text ═══ FF 01 len text Any amount of text (amount of bytes = len) for any purpose. It's best to put this event at the beginning of an MTrk. Although this text could be used for any purpose, there are other text-based Meta-Events for such things as orchestration, lyrics, track name, etc. This event is primarily used to add "comments" to a MIDI file which a program would be expected to ignore when loading that file. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.3. Copyright ═══ FF 02 len text A copyright message (ie, text). It's best to put this event at the beginning of an MTrk. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.4. Sequence/Track Name ═══ FF 03 len text The name of the sequence or track (ie, text). It's best to put this event at the beginning of an MTrk. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.5. Instrument ═══ FF 04 len text The name of the instrument that the track plays (ie, text). This might be different than the Sequence/Track Name. For example, maybe the name of your sequence (ie, Mtrk) is "Butterfly", but since the track is played on a piano, you might also include an Instrument Name of "Piano". It's best to put one (or more) of this event at the beginning of an MTrk to provide the user with identification of what instrument(s) is playing the track. Usually, the instruments (ie, patches, tones, banks, etc) are setup on the audio devices via MIDI Program Change events within the MTrk, particularly in MIDI files that are intended for General MIDI Sound Modules. So, this event exists merely to provide the user with visual feedback of the instrumentation for a track. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.6. Lyric ═══ FF 05 len text A song lyric (ie, text) which occurs on a given beat. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.7. Marker ═══ FF 06 len text A marker (ie, text) which occurs on a given beat. Marker events might be used to denote a loop start and loop end (ie, where the sequence loops back to a previous event). Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.8. Cue Point ═══ FF 07 len text A cue point (ie, text) which occurs on a given beat. A Cue Point might be used to denote where a WAVE (ie, sampled sound) file starts playing, where the text would be the WAVE's filename. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.3.2.9. End of Track ═══ FF 2F 00 This event is NOT optional. It must be the last event in every MTrk. It's used as a definitive marking of the end of an MTrk. Only 1 per MTrk. ═══ 2.3.2.10. Tempo ═══ FF 51 03 tt tt tt Indicates a tempo change. The 3 data bytes of tt tt tt are the tempo in microseconds per MIDI quarter note. Another way of expressing "microsecs per quarter note" is "24ths of a microsecs per MIDI clock" since there are 24 midi clocks in each quarter note. Representing tempos as time per beat instead of beat per time allows exact, long-term synch with time-based protocols like SMPTE. To convert this value to beats per minute: TempoBPM = 60,000,000/(tt tt tt) For example, a tempo of 120 BPM = 07 A1 20. ═══ 2.3.2.11. SMPTE Offset ═══ FF 54 05 hr mn se fr ff Designates the SMPTE start time (hours, minutes, secs, frames, subframes) of the Mtrk. It should be at the start of the MTrk. The hour should not be encoded with the SMPTE format as it is in MIDI Time Code. In a format 1 file, the SMPTE OFFSET must be stored with the tempo map (ie, the first MTrk), and has no meaning in any other MTrk. The ff field contains fractional frames in 100ths of a frame, even in SMPTE based MTrks which specify a different frame subdivision for delta-times (ie, different from the subframe setting in the MThd). ═══ 2.3.2.12. Time Signature ═══ FF 58 04 nn dd cc bb Time signature is expressed as 4 numbers. nn and dd represent the "numerator" and "denominator" of the signature as notated on sheet music. The denominator is a negative power of 2: 2 = quarter note, 3 = eighth, etc. The cc expresses the number of MIDI clocks in a metronome click. The bb parameter expresses the number of notated 32nd notes in a MIDI quarter note (24 MIDI clocks). This event allows a program to relate what MIDI thinks of as a quarter, to something entirely different. For example, 6/8 time with a metronome click every 3 eighth notes and 24 clocks per quarter note would be the following event: FF 58 04 06 03 24 08 ═══ 2.3.2.13. Key Signature ═══ FF 59 02 sf mi sf = -7 for 7 flats, -1 for 1 flat, etc, 0 for key of c, 1 for 1 sharp, etc. mi = 0 for major, 1 for minor ═══ 2.3.2.14. Proprietary Event ═══ FF 7F len data... This can be used by a program to store proprietary data. The first byte(s) should be a unique ID of some sort so that a program can identity whether the event belongs to it, or to some other program. A 4 character (ie, ascii) ID is recommended for such. Note that len could be a series of bytes since it is expressed as a variable length quantity. ═══ 2.4. Errata ═══ In a format 0 file, the tempo and time signature changes are scattered throughout the one MTrk. In format 1, the very first MTrk should consist of just the tempo and time signature events so that it could be read by some device capable of generating a "tempo map". In format 2, each MTrk should begin with at least one initial tempo and time signature event. Note: If there are no tempo and time signature events in a MIDI file, assume 120 BPM and 4/4. ═══ 3. MIDIFILE.DLL ═══ MIDIFILE.DLL is an OS/2 2.X Dynamic Link Library (DLL) that a programmer can use to simplify writing OS/2 applications that read and write MIDI files. MIDIFILE.DLL does most of the generic "bookkeeping" duties of parsing MIDI files in a way that allows some flexibility as to how the application implements the particulars of reading in data or writing out data. Many programs can be using that one copy of the DLL simultaneously, thus reducing redundant code that eats up RAM needlessly. Furthermore, the DLL helps to eliminate discrepancies and incompatibilities in the way that two programs read and write MIDI files if both programs use this DLL, since both will be using the same reader/writer "manager". MIDIFILE.DLL is freely distributable with any OS/2 application, be it commercial or otherwise. There are no pagan distribution charges, manipulative licensing fees, or other blatant forms of capitalistic trickery associated with the use of MIDIFILE.DLL and its examples. My intent is to provide OS/2 programmers with a versatile development aid that will hopefully enjoy widespread use, and thus make OS/2 MIDI development more standardized. As long as they don't impede development, standards are very important. Applications that work well together make each other more useful. Typically, an end-user will use two such programs with the same data files, utilizing the strengths of one product to compensate for the weaknesses of the other product. He ends up with a more powerful system. That helps everyone from the end-users to the folks who sell software. It's a simple concept, but you'd be surprised how many businessmen latch onto the self-destructive notion that they must fight and thwart their "competitors". Often, this makes things worse for even the end-users. The only thing that you can't do with MIDIFILE.DLL is to prevent someone else from distributing it and using it. ═══ 3.1. Archive ═══ The MIDIFILE archive consists of the DLL and several example applications with source code. The DLL itself is named MIDIFILE.DLL, and must be copied to one of the directories specified by the LIBPATH statement in your config.sys file. Usually, one of the specified directories is .;, which means that the DLL can be placed in the same directory as the applications. The applications will not run if the DLL isn't placed somewhere in your LIBPATH. These applications are designed to be run from an OS/2 command line. The applications are as follows. In the Syntax, note that arguments within [ ] are optional. MfRead Reads a MIDI file and displays information about its contents. It lists the chunks found in a MIDI file, and the contents of MThd and MTrk chunks. For MTrk chunks, it can list information about every event in the track, or just a count of how many events of each type are found (if the /i switch is specified). You can use this utility to discover what Format a MIDI file is, how many tracks are contained therein, what kind of events are within the MTrks, etc. You can also use it to check MIDI files that your own programs create to ensure that these files comply with the MIDI file spec. This is an example of how to write a program that uses MIDIFILE.DLL to read in a MIDI file. Syntax: MfRead.exe filename [/i] MfWrite Writes a dummy MIDI file in one of the 3 MIDI Formats. This example has no practical use. It exists just to show you how to write a program that uses MIDIFILE.DLL to save a MIDI file. Syntax: MfWrite.exe filename [0, 1, or 2 (for the Format)] MfToVlq Takes the values typed in as numeric args, and shows each one as a variable length quantity (ie, a series of bytes). For example, if you type: MfToVlq 13 0x4000 then MfToVlq will show you 13 expressed as a variable length quantity, and then hex 4000 as a variable length quantity. Note that you indicate a hex value by prefacing it with a 0x. Syntax: MfToVlq.exe Value1 [Value2...] MfVlq Takes a variable length quantity (ie, a series of bytes) typed in as numeric args, and combines them into one value. This is the opposite of MfToVlq. To express a byte as hex, preface it with a 0x. Syntax: MfVlq.exe variable length quantity bytes... Each application has a MAKE file to compile it into an executable. These executables were made with IBM's C/Set2 compiler, Link386, and NMAKE.EXE (the latter 2 come with the ToolKit). The .def and .dep files are used by these development tools. The MIDIFILE.LIB file is a link library that gets linked with your C application. You should tell the linker that it needs to link with this library. Note that the make files specify it as a link library to Link386. Also, your C source code must include a reference to the C include file found in this archive, like so: #include Put the above line after the reference to the standard os2.h include file. Note: If you need to make a MIDI file for test purposes, use MfWrite. ═══ 3.2. MidiReadFile() and MidiWriteFile() ═══ The DLL has 2 functions to read and write a complete MIDI file, MidiReadFile() and MidiWriteFile() respectively. These 2 functions initiate and carry out their respective operations with one call. When you return from one of these functions, you will have either read or written a complete MIDI file. For example, calling MidiWriteFile() creates and writes out all of the data to a MIDI file. Before you call either of these functions, you must allocate and initialize certain fields of 2 special structures of my own design. These structures are described in midifile.h. ═══ 3.3. Callbacks ═══ Let's say that you have a secretary. You ask her (or him) to prepare a letter to a certain customer. From this request, the secretary automatically knows to get a sheet of paper, type the return address, and type the customer's address, because all letters have these standard features. But, beyond this, the secretary needs to get more feedback from you as to what to put into the body of the letter. So too, the DLL automatically does certain things that all applications will need to do with MIDI files. For example, all MIDI files must be opened before data can be read or written to them. So, it makes sense to have the DLL do that. But, the DLL doesn't know what to do with data that it reads in, nor does it know what data to write out to a MIDI file. So, the DLL periodically needs to get feedback from your program while it's reading or writing a file. It does this using 2 special data structures, and callbacks. What's a callback? A callback is simply a function in your C program that the DLL itself calls. You will most likely have several callbacks, and each will be called by the DLL at a certain point while the DLL is reading or writing a file. For example, your program calls MidiReadFile() to read in a MIDI file. The DLL automatically opens the MIDI file, locates the MThd chunk within it, and loads the information contained in it. All applications that read a MIDI file would want to do that. But now, the DLL doesn't know what to do with that MThd information. After all, it's likely that different programs will have different "needs" for that data, and some programs might want to store that data in global variables within the program. So, the DLL calls a function in your program, passing the loaded MThd data. Since the function is part of your program, you can assess all of your global variables, and call other functions within your program (or functions in a DLL or OS/2 system functions, etc) just like any other function in your program. Perhaps this callback might inspect the passed MThd data, and set certain global variables as a result. When the callback returns, control is returned to the DLL, and the DLL carries on with the next generic step of reading in a MIDI file. For example, at this point, it makes sense that any application would want to read in the header for the next chunk to see what kind of chunk it is. So, the DLL does that. Let's say that the DLL finds an MTrk header. Once again, the DLL doesn't know what the application wants to do with this MTrk, so the DLL calls another function in your program that expects to be called when an MTrk chunk is encountered. This process of the DLL calling your callback functions continues until the DLL has read in all of the data within the MIDI file, and then MidiReadFile() finally returns to where you made that call in your program. As a further illustration, look at the following code. ULONG value; VOID parent() { child(); return; } VOID child() { baby1(); baby2(); return; } VOID baby1() { value=1; return; } VOID baby2() { return; } parent() calls child() which then calls baby1() and baby2(). This is fairly obvious. The call to child() doesn't return until child() itself returns, and that's after baby1() and baby2() get executed. Now let's assume that you put child() in another source code module. Does that mean that parent() can't call child(), or that child() can't call baby1() and baby2()? No. Is the logic changed at all? No. child() is simply in a different module. Now think of child() as being MidiReadFile(). Just because it's in the DLL doesn't mean that parent() can't call it. Neither does that mean that MidiReadFile() can't call baby1() and baby2(), which I refer to as callbacks. All of your callbacks must be declared to be of type EXPENTRY and returning a LONG (which is an error code). For example, our baby2() callback would be: LONG EXPENTRY baby2(MIDIFILE * mf) { return; } Usually, the DLL passes a MIDIFILE structure to your callback (ie, the same one that you pass to MidiReadFile or MidiWriteFile) which will contain pertinent data. The concept of callbacks is really quite simple, but since you can't "see" the code to MidiReadFile() calling your callbacks, it may be harder to visualize the logic. Just remember that one of your functions calls MidiReadFile() which then calls a series of your callbacks until the MIDI file is read in, and then MidiReadFile() finally returns to your function that called it. ═══ 3.4. CALLBACK and MIDIFILE structures ═══ So, how does the DLL know which of your program's callbacks to call? Well, that's what one of these special structures is for. You place pointers to your functions into the CALLBACK structure. Each of the fields in the CALLBACK is for a pointer to a function in your program. There's a field where you place a pointer to a function that the DLL should call after it has located and loaded an MThd chunk. There's a field where you place a pointer to a function that the DLL should call when it encounters a Time Signature Meta-Event within an MTrk. Etc. For some of the CALLBACK fields, you could set them to 0 instead of supplying a pointer to a callback. In this case, the DLL will instead perform some default action when it gets to the point where it should call that "missing" callback. The other structure is a MIDIFILE. Mostly, the MIDIFILE is used to maintain variables that are used during the reading and writing of MIDI files. Also, while reading a MIDI file, it is often used to contain loaded data from the MIDI file, which is then passed to one of your callbacks. While writing a MIDI file, your callbacks often place data into the MIDIFILE structure and then upon return, the DLL writes that data out to the MIDI file. The midifile.h has comments that describe the fields of the MIDIFILE and CALLBACK structures. ═══ 3.5. Initialization ═══ Before calling MidiReadFile() or MidiWriteFile(), you must setup certain fields of the CALLBACK and MIDIFILE structures. All of the CALLBACK fields must be set either to a pointer to some callback, or 0 (if you want the DLL's default action for that callback). I'll discuss when each callback gets called, and what it should do, later in the discussion of reading and writing MIDI files. The CALLBACK fields only need to be initialized once when you first create the CALLBACK. If your CALLBACK is a declared global in your program, you could initialize the fields within the declaration. Unless you wish to later change a CALLBACK field to point to a different function, you don't need to ever reset the CALLBACK fields. It should be noted that you can change a CALLBACK field at any time, even within a callback function. Note: Unless you want your program to do the actual reading and writing of bytes to disk, perhaps in order to use buffered file I/O routines such as fread(), fwrite(), etc, you'll set the CALLBACK's OpenMidi, ReadWriteMidi, SeekMidi, and CloseMidi fields to 0. Not all of the MIDIFILE fields need to be initialized. Many fields are setup and maintained by the DLL. The fields that you must setup are Handle, Flags, and Callbacks. Callbacks is simply a pointer to your CALLBACK structure. (ie, You pass the MIDIFILE structure to MidiReadFile() or MidiWriteFile() and the DLL gets your CALLBACK from the MIDIFILE). Flags is a USHORT. There are 16 bits in this USHORT, each one being a flag that is either "on" (set) or "off" (clear). These flags give you choices as to how the DLL processes data, or sometimes indicate the "state" of something. See midifile.h for details. Typically, you will simply clear these bits for default operation. Finally, Handle is a pointer to the null-terminated MIDI filename that you want the DLL to open for reading or writing. For example, you might initialize it as so: struct MIDIFILE mfs; UCHAR name[] = "C\:MyFile.mid"; mfs.Handle = (ULONG)&name[0]; The ULONG cast is just to prevent a compiler warning message. (After the DLL opens the file, it then uses the Handle field to store the file handle). ═══ 3.6. Fixed/Variable Length Events ═══ Within an MTrk chunk, I differentiate between fixed and variable length events. What's a fixed length event ? That's simply an event that always has the same number of bytes in it. For example, a MIDI Note-On event always has 3 bytes; the status, the note number (ie, pitch), and the velocity. (The DLL resolves running status, always providing you with a Status byte for each event). A MIDI Program Change event always has 2 bytes; the status, and the program number. A Tempo Meta-Event always has 6 bytes; 0xFF, 0x51, 0x03, and the 3 bytes that comprise the micros per quarter note. The following are considered to be fixed length events: 1. MIDI events with a Status less than 0xF0. These include MIDI Note-Off, Note-On, Program Change, Aftertouch, Polyphonic Pressure, Controllers, and Pitch Wheel events on any of the 16 channels. All of these have 3 bytes, except Program and Pressure which have 2. 2. MIDI REALTIME and SYSTEM COMMON events except for SYSTEM EXCLUSIVE. These include events with a Status of 0xF1 to 0xFE, except 0xF7. (0xFF is interpreted as a Meta-Event status). 3. The Meta-Events of Sequence Number, End Of Track, Tempo, SMPTE Offset, Time Signature, and Key Signature. Each one has its own defined length. What's a variable length event ? That's simply an event that may have any number of bytes in it. For example, a MIDI SYSTEM EXCLUSIVE event for a Roland RAP-10 audio card may be a different length than a SYSEX event for an EMU Proteus. Furthermore, the following Meta-Events all can be various lengths depending upon the text stored in each: Text, Copyright, Track Name, Instrument Name, Lyric, Marker, and Cue Point. ═══ 3.7. Reading a file ═══ Let's discuss the procedure for reading a file. First, you'll initialize the CALLBACK and MIDIFILE structures as described in Initialization, and then call MidiReadFile(), passing it your MIDIFILE structure. The DLL will open the file, locate the MThd chunk, and read that info into the MIDIFILE's Format, NumTracks, and Division fields. These fields directly correspond to the values in an MThd, except that these fields are arranged in Intel reverse byte order. (ie, Your C program can read the Format as a USHORT, and not have to flip the two bytes). The MIDIWRITE bit of Flags will be cleared by the DLL. Then, the DLL will call your StartMThd callback if supplied, passing a pointer to your MIDIFILE structure. Your callback might want to inspect the Format, and perhaps adjust certain global variables (or CALLBACK pointers) depending upon whether Format 0, 1, or 2. Or, you might need to allocate as many structures as are needed to hold NumTrack number of MTrks. The Format, NumTracks, and Division of the MIDIFILE remain in the structure all of the way through MidiReadFile(), so you don't necessarily need to copy these values anywhere else. The MIDIFILE's FileSize always reflects how many more bytes still need to be read from the file. This is maintained by the DLL and should not be altered. Likewise, the ChunkSize field reflects how many more bytes still need to be read from the current chunk. (This will be 0 at this point since the entire MThd was read in). The TrackNum field always tells you what MTrk number has last been read in. (This will be 0xFF right now since no MTrk chunks have yet been read). This number is also maintained by the DLL (ie, it is incremented each time that the DLL encounters an MTrk). The DLL maintains the Handle field, and this should not be altered. Your callback should return a 0 for success (to let the DLL continue reading the file). Otherwise, your DLL should return a non-zero number to abort the read. This number will then be returned from MidiReadFile(). Note: All callbacks return 0 for success, or non-zero for an error. All are passed one argument; your MIDIFILE (which is sometimes redeclared to be another structure similiar to the MIDIFILE, but it's really always a MIDIFILE). After your StartMThd callback successfully returns, the DLL reads in the next chunk. If this is not an MTrk chunk, your UnknownChunk callback is called. The ID of the chunk is in the MIDIFILE's ID. Note that the 4 bytes are stored as a ULONG for easy comparison to a CONSTANT, but the byte order is Big Endian (ie, Motorola). You could use the DLL function MidiFlipLong() to switch it to Intel order. ChunkSize indicates the number of data bytes contained in the chunk. The DLL has not loaded these bytes. If you want to load them, your callback must do that now. You must use the DLL function MidiReadBytes() to load data from the MIDI file. If you don't supply an UnknownChunk, the DLL skips unknown chunks. Note: Never read bytes using some OS or C library function. Always use MidiReadBytes(), which is used in much the same manner as you might use DosRead(). Alternately, if you simply want the DLL to skip over this chunk's data, your callback need do nothing more than return. The DLL will skip ahead to the next chunk. You could even read some, but not all, bytes from the chunk, and then return to let the DLL skip the remaining bytes. On the other hand, if the chunk is an MTrk, then the DLL begins the process of calling a series of your callbacks to read in the events within the chunk. First, the DLL calls your StartMTrk callback, if supplied. The DLL will have incremented the MIDIFILE's TrackNum. (For the first MTrk, this will be 0). You might use the StartMTrk callback to initialize global variables for reading in a track. If you want the library to read in the entire MTrk chunk into a buffer, and then return this to you, your callback should allocate a buffer to contain ChunkSize bytes, and store the pointer in the MIDIFILE's Time field. In this case, upon return, the DLL will load the chunk, and then call your StandardEvt callback (which you must supply in this case), which can then process the buffer as you see fit. If you want the DLL to load one event at a time, your StartMTrk should return with the Time field left at 0 (ie, the DLL initially sets it to 0). At this point, the DLL will load the first event, and then call one of your callbacks, depending upon what type of event it is (and if you have supplied a callback for that type of event). The following discussion of the individual callbacks is for when the DLL loads one event at a time. The basic idea is that the DLL loads one event (or for a variable length event, just its Status and Length), figures out what type of event it is, and calls your respective callback which is expected to store the data somewhere. When your callback returns, the DLL loads the next event, etc, until all events in the MTrk have been loaded. If you don't supply a callback for that event type, the DLL skips that one event. For all event types, the DLL always stores the event's time in the MIDIFILE's Time field. In other words, the DLL converts the event's variable length quantity timestamp into 1 ULONG and stores it in the MIDIFILE's Time. This time is referenced from 0, rather than the previous event like within the MIDI file, unless you set the MIDIDELTA Flag. For MIDI events with status 0x80 to 0xEF, the DLL will load the event entirely into the MIDIFILE. The MIDI Status byte will be placed into the MIDIFILE's Status field, and the 1 or 2 MIDI data bytes will be placed in MIDIFILE's Data1 and Data2. (If the event is a PROGRAM CHANGE or POLYPHONIC PRESSURE, which have only 1 data byte, the second byte is set to 0xFF). The DLL always provides a Status, resolving MIDI running status on your behalf. Then, the DLL calls your StandardEvt callback, if supplied. Typically, your callback will extract those fields, along with the Time, and store the bytes in some "track memory". If the event is a SYSEX, SYSEX CONTINUATION, or ESCAPE, then the DLL calls your SysexEvt callback, if supplied. Since these events can have an arbitrary number of data bytes, the DLL only loads the Status (ie, 0xF0 or 0xF7) into the MIDIFILE's Status, and sets MIDIFILE's EventSize to the number of data bytes that need to be read in. Your callback is expected to read in those data bytes using MidiReadBytes(). Typically, you'll load/store the data in some allocated memory. Alternately, you can choose to have the DLL skip over those bytes by simply returning. The DLL sets the MIDISYSEX Flag when it first encounters an F0 event, and clears this flag when it later encounters an event that would definitely indicate that no more SYSEX CONTINUATION "packets" follow. For a Sequence Number Meta-Event, the DLL redefines the MIDIFILE as a METASEQ structure. If you compare the 2 structures, you'll note that they're both the same size, and have most of the same fields. They really are the same. It's just that the MIDIFILE's Data fields have been redefined specifically for a Sequence Number Meta-Event. (Many of the other type of Meta-Events also use some sort of redefinition of a MIDIFILE). The DLL loads the sequence number into the METASEQ's SeqNum field. The byte order is Intel form, so the field can be read as a USHORT. Then, the DLL calls your MetaSeqNum callback if supplied, passing the METASEQ. For a Tempo Meta-Event, the DLL redefines the MIDIFILE as a METATEMPO structure. The DLL loads the micros per second into Tempo using Intel byte order so that it can be read as a ULONG. The DLL also calculates the tempo in beats per minute and stores that in the METATEMPO's TempoBPM. Then, the DLL calls your MetaTempo callback if supplied. For a SMPTE Offset Meta-Event, the DLL redefines the MIDIFILE as a METASMPTE structure. The DLL loads the hours, minutes, seconds, frames, and subframes fields into the respective METASMPTE fields. Then, the DLL calls your MetaSmpte callback if supplied. For a Time Signature Meta-Event, the DLL redefines the MIDIFILE as a METATIME structure. The DLL loads the nom, denom, etc, into the respective METATIME fields. The DLL will express the denom as the actual denom of the time signature (instead of a power of 2) if you have set your MIDIFILE's MIDIDENOM Flag. Then, the DLL calls your MetaTimeSig callback if supplied. For a Key Signature Meta-Event, the DLL redefines the MIDIFILE as a METAKEY structure. The DLL loads the key and minor fields into the METAKEY. Then, the DLL calls your MetaKeySig callback if supplied. For an End of Track Meta-Event, the DLL redefines the MIDIFILE as a METAEND structure. Then, the DLL calls your MetaEOT callback if supplied. Note that this will always be the last event in an MTrk. For Meta-Events of types 0x01 to 0x0F (ie, Text, Copyright, Instrument, Track Name, Marker, Lyric, Cue Point), 0x7F (Proprietary), and any other currently undefined Meta-Events, these can be of arbitrary length. So, the DLL loads the meta Type byte into the MIDIFILE's Status field, and sets the MIDIFILE's EventSize to the number of data bytes that need to be read in. Then, the DLL calls your MetaText callback if supplied. Your callback is expected to read in those data bytes using MidiReadBytes(). Typically, you'll load/store the data in some allocated memory. Alternately, you can choose to have the DLL skip over those bytes by simply returning. If you didn't supply a callback for a particular event type (ie, you set the respective CALLBACK field to 0), then the DLL will skip loading that event. For example, if you set the CALLBACK's MetaTempo field to 0, then the DLL will skip over all Tempo Meta-Events. The DLL continues reading in the next event (unless your callback returns non-zero to abort), and calls your respective callback for that event, until all events, including the End Of Track, are read in. Then the DLL reads in the next chunk header, and the process repeats, until the remainder of the file has been parsed, at which time MidiReadFile() returns. MidiReadFile() itself returns a 0 if there were no errors reading the file. Note: The DLL resolves MIDI running status, always supplying a status byte in the MIDIFILE's Status field for each event. If you don't want to store those extra Status bytes (ie, you want to implement running status in your track memory), then you can compare the MIDIFILE's RunStatus field with the Status field in your StandardEvt callback. If both are the same, then you've got an event whose status was resolved (ie, it should be running status). Note that Meta-Events have nothing to do with running status since these aren't real MIDI events. Furthermore SYSEX never have running status. So, don't check for running status in any Meta callback or the SysexEvt callback. ═══ 3.8. Writing a file ═══ Let's discuss the procedure for writing (creating) a file. First, you'll initialize the CALLBACK and MIDIFILE structures as described in Initialization, and then call MidiWriteFile(), passing it your MIDIFILE structure. The DLL will open the file. It also sets the MIDIWRITE Flag. Then, the DLL will call your StartMThd callback, passing a pointer to your MIDIFILE structure. Your callback should initialize the MIDIFILE's Format, NumTracks, and Division fields. These fields directly correspond to the values in an MThd, except that these fields are arranged in Intel reverse byte order. (ie, Your C program can write the Format as a USHORT, and not have to flip the two bytes to Big Endian). Upon return, the DLL will write out the MThd chunk. Alternately, you could initialize these MIDIFILE fields before calling MidiWriteFile(), in which case you don't need the StartMThd callback (ie, set that CALLBACK field to 0). Then, the DLL starts calling various callbacks to write out the MTrk chunks. It repeats the following process for as many times as your MIDIFILE's NumTracks. Your callbacks should return a 0 for success (to let the DLL continue writing the file). Otherwise, your DLL should return a non-zero number to abort the write. This number will then be returned from MidiWriteFile(). The DLL increments the TrackNum field for each MTrk to be written out. (The first MTrk is 0). It also writes out the MTrk header. (The DLL takes care of setting the chunk's size properly). The DLL maintains the FileSize field as the number of bytes written out to the file (including chunk headers). This should not be altered. The DLL also maintains the ChunkSize field, which should not be altered (and not used as a reflection of how many bytes have been written to a chunk). The DLL maintains the Handle field, and this should not be altered. First, the DLL calls your StartMTrk callback, if supplied. Typically, you might use this to set up some globals associated with the MTrk to be written. Optionally, you could write out some proprietary chunks before this MTrk, using MidiWriteHeader(), 1 or more calls to MidiWriteBytes(), and MidiCloseChunk(). If you want to supply a buffer that is already formatted with an entire MTrk's data (ie, minus the header), place a pointer to the buffer in the MIDIFILE's Time field. Upon return, the DLL will write out that entire buffer, and then call your StandardEvt callback once, if supplied. Alternately, if you want to write out the MTrk data one event at a time, then return with Time left at 0. The following discussion of the individual callbacks is for when the DLL writes one event at a time. The basic idea is that the DLL calls your StandardEvt callback for each event to be written out. Your callback stuffs the data for one event into the MIDIFILE (or for a variable length event, just its Status and Length), and returns to the DLL which writes out that event (perhaps calling another callback for a variable length event). The DLL continues calling StandardEvt until you return an End Of Track event to write out. Then, the process repeats for the next MTrk chunk. Before calling StandardEvt initially, the DLL calls your MetaSeqNum callback once, if supplied. Typically, this is used to write out a Sequence Number Meta-Event (which must be first in the MTrk if desired), and any other events that need to written once at the start of the MTrk. The DLL redefines the MIDIFILE as a METASEQ, passing that to your callback. At the least, your callback should return the METASEQ with the SeqNum and NamePtr set as desired. SeqNum is in Intel format, and can be written as a USHORT. If you also want the DLL to write out a Sequence Name event, place a pointer to the null-terminated name in the METASEQ's NamePtr field. Otherwise, if you don't want a name event, set this to 0. Upon return, the DLL writes out that event(s). Optionally, your callback can write additional events before returning, using MidiWriteEvt(), but the callback must return at least one event in the METASEQ for the DLL to write out. You need to set up the METASEQ (which is really a MIDIFILE and so can be recast as any of the structures in midifile.h) before calling MidiWriteEvt(), and the manner that you do so is the same as your StandardEvt callback described below. For all events that you wish to return to the DLL for writing out, StandardEvt must store the event's time in the MIDIFILE's Time field. This time is referenced from 0, rather than the previous event like within the MIDI file, unless you set the MIDIDELTA Flag. The DLL will write out the event's time as a variable length quantity timestamp to the MIDI file. For MIDI events with status 0x80 to 0xEF, 0xF1, 0xF2, 0xF3, 0xF6, 0xF8, 0xFA, 0xFB, 0xFC, or 0xFE, your callback stores the event entirely into the MIDIFILE. The MIDI Status byte must be placed into the MIDIFILE's Status field, and any MIDI data bytes must be placed in MIDIFILE's Data1 and Data2. Upon return, the DLL writes out the entire event. If the event is a SYSEX or SYSEX CONTINUATION (0xF0 or 0xF7), then you must store that Status in the MIDIFILE's Status, and set the EventSize to the number of data bytes that will follow the Status. Finally, set the ULONG starting at Data[2] to 0. Upon return, the DLL writes out the Status and the size as a variable length quantity. Then it calls your SysexEvt callback, which should write out the expected data bytes using MidiWriteBytes(). Note: Never write bytes using some OS or C library function. Always use MidiWriteBytes(), which is used in much the same manner as you might use DosWrite(). Alternately, StandardEvt could store the F0 or F7 Status, the EventSize, and then set the ULONG at Data[2] to be a pointer to a buffer to write out. Upon return, the DLL will write out the event entirely, with the data in the buffer. Your SysexEvt callback will not be called (and need not be supplied). For a Tempo Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METATEMPO, store an 0xFF in the Type field, store 0x51 in the WriteType field, and store the micros per quarter in the Tempo field. Tempo is a ULONG in Intel format (ie, the DLL writes out the proper Big Endian bytes). Alternately, if you've set the MIDIBPM Flag, you set the TempoBPM field instead, and the DLL will calculate the Tempo field. For a SMPTE Offset Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METASMPTE, store an 0xFF in the Type field, store 0x54 in the WriteType field, and store the hours, minutes, seconds, frames, and subframes fields. For a Time Signature Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METATIME, store an 0xFF in the Type field, store 0x58 in the WriteType field, and store the nom, denom, etc, fields. If you've set the MIDIDENOM Flag, you store the denom field as the true time signature denominator, and the DLL will convert it to a power of 2. For a Key Signature Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METAKEY, store an 0xFF in the Type field, store 0x59 in the WriteType field, and store the key and minor fields. For an End Of Track Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METAEND, store an 0xFF in the Type field, and store 0x2F in the WriteType field. Note that this will be the last event in the MTrk. Returning this event to the DLL for writing signals that the MTrk chunk is finished. For a Sequence Number Meta-Event, your StandardEvt callback should redefine the MIDIFILE as a METASEQ, store an 0xFF in the Type field, store 0x00 in the WriteType field, and store the SeqNum field. Set the NamePtr field to 0 if you don't want a Sequence Name event also written out. Otherwise, set this to point to a null-terminated name. For Meta-Events of types 0x01 to 0x0F (ie, Text, Copyright, Instrument, Track Name, Marker, Lyric, Cue Point), 0x7F (Proprietary), and any other currently undefined Meta-Events, set the MIDIFILE's Status to 0xFF, and set Data[0] to the meta type. If you want the DLL to write out the subsequent data bytes, set the ULONG at Data[2] to point to a buffer containing the data to write out, and set EventSize to the number of data bytes to write. If you set EventSize to 0, then the DLL assumes that the buffer contains a null-terminated string, and calculates the length. In these cases, the DLL writes out the event entirely. Alternately, if you set the ULONG at Data[2] to 0, then you must set the EventSize to the number of subsequent bytes that you intend to write out. In this case, the DLL calls your MetaText callback which will write out those bytes using MidiWriteBytes(). The DLL continues calling StandardEvt (and perhaps SysexEvt or MetaText) to write out each event (unless your callback returns non-zero to abort), until an End Of Track event is written. Then the DLL starts the process again with the next MTrk chunk, until all MTrks have been written (ie, NumTracks). Lastly, the DLL calls your UnknownChunk callback, if supplied. This callback could write out some proprietary chunks at the end of the file, using MidiWriteHeader(), 1 or more calls to MidiWriteBytes(), and MidiCloseChunk(). After this, MidiWriteFile() returns. MidiWriteFile() itself returns a 0 if there were no errors writing the file. ═══ 3.9. Errors ═══ Your callbacks return 0 for success, or non-zero for an error. The error numbers that might be returned by the DLL itself are listed in midifile.h. Note that the DLL returns positive numbers for any errors. You may wish to have your callbacks return negative numbers to help you differentiate between an error that happened within the DLL, and an error occuring within your callbacks. The only limitation that is imposed is that, if you supply a MidiReadWrite callback, it can't return -1. The DLL has a routine, MidiGetErr(), which can return a null-terminated string that describes one of the error numbers defined in midifile.h (ie, for display to the end-user). It copies the string to a buffer that you supply, and returns the string length. If the error number that you pass is not one of the ones defined by the DLL, the buffer is nulled, and 0 returned. In this case, the error was obviously one that a callback returned. ═══ 3.10. Dual function callbacks ═══ Both MidiReadFile() and MidiWriteFile() require MIDIFILE and CALLBACK structures. Both functions call some of the same callbacks. For example, MidiWriteFile() calls your StandardEvt callback, expecting it to return info about the next event to write out. MidiReadFile() also calls your StandardEvt callback, but it expects it to store the info that the DLL has loaded into the MIDIFILE. If your application both reads and writes MIDI files, you could use the same MIDIFILE structure for calls to MidiReadFile() and MidiWriteFile(), but you would need 2 CALLBACK structures. One would have pointers to callbacks that expect to be writing out a MIDI file. Before calling MidiWriteFile(), you would place a pointer to this CALLBACK into the MIDIFILE's Callbacks. The other CALLBACK would have pointers to callbacks that expect to be reading a MIDI file. Before calling MidiReadFile(), you would place a pointer to this CALLBACK into the MIDIFILE's Callbacks. Alternately, you could use one CALLBACK and dual function callbacks. These are simply callbacks that inspect the MIDIWRITE bit of MIDIFILE's Flags in order to decide whether a read or write operation needs to be performed. Remember that the DLL always sets this for a write operation, and clears it for a read operation. So, for example, your StandardEvt callback could inspect this Flag, and know whether it was being called via MidiReadFile() or MidiWriteFile(). In this case, you only need to initialize the MIDIFILE's Callbacks once at the start of the program. Not all callbacks would need to inspect this flag. Although MidiReadFile() might call all of your supplied callbacks, MidiWriteFile() never calls the MetaTempo, MetaSmpte, MetaTimeSig, MetaKeySig, and MetaEOT callbacks. These callbacks should always assume that they're doing a read operation. Furthermore, MidiWriteFile() may never call your SysexEvt and MetaText callbacks if your StandardEvt callback always supplies a pointer to a data buffer for SYSEX and variable length meta events. ═══ 3.11. File I/O ═══ Usually, the DLL takes care of actually reading and writing bytes to a file. The DLL's reading and writing routines are not buffered (although the DLL has been designed to implement as efficient an unbuffered scheme as possible with MIDI files). If you wish to instead use your own, buffered file I/O routines to read/write to the file (such as fread(), fwrite(), fseek(), etc), instead of letting the DLL do that, then you need to supply the callbacks for MidiOpen, MidiReadWrite, MidiSeek, and MidiClose. When initializing the MIDIFILE before calling MidiReadFile() or MidiWriteFile(), you don't need to place a pointer to the filename in the Handle. Instead, when your MidiOpen callback is called, it should open the user's filename and store the fileHandle for later use. (You'll probably use the MIDIFILE's Handle field for storage). Remember that the MIDIWRITE Flag is set if the DLL is writing out a MIDI file, and clear if the DLL is loading a file. So, your callback can determine whether it needs to open a file for reading or writing. For loading, you should also set the MIDIFILE's FileSize to the number of bytes that the DLL has to parse (ie, usually the size of the file itself). For writing, you should set the FileSize to where the current file pointer is located (ie, 0 if you're at the beginning of the file). If you're using the DLL to load a MIDI file that is buried inside of a larger file that you've already read some initial data from, you should set FileSize to the remaining bytes after the current file pointer position. If you're writing out a MIDI file inside of a larger file that you've already written some initial data to, you should set FileSize to the amount of bytes already written out. Note: It's possible to open the file, and setup the MIDIFILE's FileSize field before calling MidiReadFile() or MidiWriteFile(), but you still need a MidiOpen callback, which should simply return 0. Your MidiReadWrite callback is passed a pointer to your MIDIFILE structure, a pointer to a buffer, and a length argument. The first thing that your callback should do is examine the MIDIWRITE Flag bit to see if you need to perform a read or write operation. For a write operation, the buffer pointer contains the data to be written out, and the length is the number of bytes to write. For a read operation, the length is the number of bytes to read in, and the buffer is where to read those bytes. Note: None of your other callbacks should ever call your MidiReadWrite callback directly. Your callbacks should use either MidiReadBytes() or MidiWriteBytes(), which indirectly call your MidiReadWrite callback to do the actual reading/writing of bytes. Your MidiSeek callback should expect to do a seek. It is passed a pointer to the MIDIFILE, a signed long representing the amount of bytes to seek, and the ulMoveType as described by OS/2's DosSetFilePtr(). (The DLL only ever passes FILE_CURRENT as the movement type). Note: None of your other callbacks should ever call your MidiSeek callback directly. Your callbacks should use the DLL's MidiSeek(), which indirectly calls your MidiSeek callback to do the actual seek operation. MidiSeek() only allows seeking from the current file pointer position. Your MidiClose callback should expect to close the file that you opened. Of course, you could chose to not close it at this time, and then after returning from MidiWriteFile() or MidiReadFile(), do some more reading/writing to the file. But, it's recommended that you instead read/write any final bytes in your UnknownChunk callback. If your MidiClose callback closes the file, then you can be assured that the file will be properly closed, even if an error occurs and the read/write is aborted. ═══ 3.12. Limitations ═══ Since MidiReadFile() and MidiWriteFile() carry out the desired operation in entirety before returning, it's not possible to have one thread read in data from one MIDI file at the same time that it's writing out data to another MIDI file. You'd have to read in one file entirely into memory, and then write it back out. Alternately, you could have two threads; one to call MidiReadFile() and one to call MidiWriteFile(), and have the callbacks of one thread wait for semaphores or messages from the other thread's respective callbacks to synchronize reading/writing. You'd need separate MIDIFILE structures.