home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OS/2 Shareware BBS: 10 Tools
/
10-Tools.zip
/
midifil2.zip
/
midifile.INF
(
.txt
)
< prev
next >
Wrap
OS/2 Help File
|
1995-07-21
|
38KB
|
908 lines
ΓòÉΓòÉΓòÉ 1. Preface ΓòÉΓòÉΓòÉ
MIDIFILE Programmer's Manual
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.
Note: Although the description of this file format is available from the MIDI
Manufacturers Association, I wrote a more explicit and "down to earth"
explanation in the form of a nicely organized, online OS/2 book. Please
read The MIDI File Format section of my book, The MIDI Book before
reading this document and using the MIDIFILE.DLL. This document does
not detail the MIDI File Format.
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 also simplify application development 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. This manual describes the DLL, and the accompanying C examples
demonstrate how to use it in applications.
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".
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.
To avoid name conflicts with DLL functions and data, avoid naming any of your
functions and data starting with the letters Midi.
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.
ΓòÉΓòÉΓòÉ 2. Copyright ΓòÉΓòÉΓòÉ
This OS/2 Online Book and the related files, MIDIFILE.DLL, MIDIFILE.H,
MIDIFILE.LIB, and examples for using MIDIFILE.DLL are all copyright 1995 by
Jeff Glatt. These files are freely redistributable, and may be used by and
distributed along with any software, be it commercial or otherwise, provided
that these files are not internally modified, nor specifically sold as a
complete product by themselves. The only price that you have to pay is the one
that you're already paying by spending all of your time in front of a computer
instead of developing healthier outlets.
NOT SO STANDARD DISCLAIMER:
These programs are provided "as is" without warranty of any kind either
expressed or implied or tatooed in a place that only a few people have ever
seen, including but not limited to the implied warranties of merchantability,
fitness for a particular purpose, and the dubious assumption that the software
has been created by a sane individual who would never do anything that may hurt
you. The entire risk as to the results and performance of the programs is
assumed by you or someone who looks exactly like you. Jeff Glatt does not
guarantee that the functions in these programs will meet your requirements,
especially if your requirements involve lots of latex and some docile,
domesticated animal. Nor does Jeff Glatt warranty the programs to be
uninterruptable or error-free, although mercifully free of "General Protection
Faults". If you use said programs, you can not say anything nasty about the
author, even if the programs inadvertently cause the erasure of your collection
of X-rated GIFs of a conservative, overweight and overrated TV "personality"
plooking himself vigorously with his royalty checks from some rancid paperback.
Jeff Glatt is not responsible for any damages as a result of anything that he
has done, or hasn't done, or was supposed to do but never got around to it, and
furthermore, he doesn't even care so leave him alone, ratface. You may have
more or less protections in certain states of the union, depending upon how far
your local politician is willing to bend over for some bribe from a business
lobbyist. Just remember that Jeff Glatt has no money, so don't bother suing
him as a result of any damages caused by this OS/2 program. Tell your greasy
lawyer to go after IBM, and make sure that you pick 12 really stupid pinheads
for the jury. If swallowed, induce vomiting immediately by contemplating the
asthetics of Microsoft Windows.
OS/2 is a trademark of International Business Machines Corporation.
Windows is a trademark of Microsoft Incorporated, and furthermore, Bill Gates
is to blame for it.
If you have suggestions, comments, criticisms, and anything else other than
dollar bills, then send them to someone else because you got it for free, and
you know what you get for nothing? But, if you do need to contact the author,
then either phone some of the more prominent psychiatrict clinics in central
New York state, or try this:
Jeff Glatt
6 Sycamore Drive East
New Hartford, NY 13413
(315) 735-5350
ΓòÉΓòÉΓòÉ 2.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.
For compiling applications, you'll need MIDIFILE.H and MIDIFILE.LIB copied to
where you keep your C include files and C link libraries respectively, although
these files aren't needed by the executables you produce. Of course,
MIDIFILE.DLL should be included with your executable if you distribute that
executable, and the user should be instructed to copy MIDIFILE.DLL to his
LIBPATH.
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. 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.
ΓòÉΓòÉΓòÉ 4. 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.
ΓòÉΓòÉΓòÉ 5. 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.
ΓòÉΓòÉΓòÉ 6. 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).
ΓòÉΓòÉΓòÉ 7. 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.
ΓòÉΓòÉΓòÉ 8. 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.
ΓòÉΓòÉΓòÉ 9. 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 Data[0] and Data[1]. 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.
Note: MidiWriteFile() will typically alter the MIDIFILE's Time and Status
fields after your StandardEvt callback returns. So, never make the
assumption that these 2 fields will have the same values that you set
them to on the previous call to StandardEvt. For that matter, your
other callbacks (such as MetaText and SysexEvt) should not make the same assumption.
ΓòÉΓòÉΓòÉ 10. 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.
ΓòÉΓòÉΓòÉ 11. 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.
ΓòÉΓòÉΓòÉ 12. 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). 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.
Note: If you supply your own file I/O routines, the MIDIFILE's Handle field
will not be touched by the DLL, and so you can use it for whatever
purpose you decide.
ΓòÉΓòÉΓòÉ 13. 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.