home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OS/2 Shareware BBS: 10 Tools
/
10-Tools.zip
/
mididll.zip
/
midifile.INF
(
.txt
)
< prev
next >
Wrap
OS/2 Help File
|
1994-01-02
|
46KB
|
1,374 lines
ΓòÉΓòÉΓòÉ 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 <midifile.h>
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.