Generating simple MIDI files using Java, without using the Java Sound API
If you need to generate a MIDI file from a Java application,
in nearly all cases you're better off using the standard
Java Sound API, which has built-in support for it. Unfortunately,
not all platforms where Java runs has support for the MIDI API,
and even where it is supported it's a pretty heavy thing to use if
all you want to do is play a few notes of music.
My interest in writing MIDI files using Java comes from developing
music applications for the Android platform. Android applications
are Java-based, but the API set is limited. In particular, there
is no MIDI support. This is very strange, because the Android platform
has a MIDI renderer and can play MIDI files. Even stranger,
Android did go through a stage of including the standard MIDI
APIs, but Google took them out. So now, if you want to generate music
programmatically on Android, the only practical approach is to
have your program write a file and feed that it into the built-in
media player.
Happily, if your music-generation needs are modest, it's not all that
difficult to implement a Java class that writes a MIDI file.
By 'modest' I mean that you can get by with one track and one controller
channel, and you don't need to fiddle with such things as the tempo
and key signature in the middle of a track. Polyphony is possible, so
long as you're careful about your deltas (more on deltas below).
The complete source code for my MIDI writer fits into one
Java class (see the download link
at the end of this article), and has only about a hundred
lines of Java, not including comments and test code. But this simple
class is capable of generating MIDI files that can be played
by the Android media player, and the Windows media player, among
other things.
In this article I will describe the format of a minimal MIDI file, and describe
some of the nasties you'll have to contend with when using Java
to write one. At the end is a complete code example and some ideas
how to use it.
Format of a simple MIDI file
MIDI files can be as complicated as you like, but if all you need to do is output the notes for 'Mary had a little lamb' or something of similar complexity, you don't need all the bells and whistles. A simple MIDI file contains, as a minimum, the following elements.- A file header. If you settle on a timebase that suits you (see below) this will be the same for every file -- 14 constant bytes
- A track header. A constant four bytes
- Four bytes to indicate the amount of track data, including the track footer. This number is in big-endian format.
- The track data, which will usually consist of:
- Metadata events, most importantly the tempo. Most of the defined metadata (key signature, time signature, etc) is used by editing tools, and irrelevant to players
- Performance events -- notes, controller changes, etc
- The track footer -- four constant bytes
Java issues with MIDI files
The principle problem with manipulating MIDI in Java is that for the protocol is expressed in terms of both signed and unsigned bytes, and Java does not have an unsigned byte data type. The decision to make the byte data type signed only was, in my view, a lamentably daft one, but we're stuck with it. You can't say, in Java:byte x = 0xFF;because 0xFF is too big to fit a signed byte. It doesn't even wrap around to -1, which is what C does. It simply won't compile. Although it seems inefficient, the only way I've found to deal with this situation elegantly is to define all bytes as (signed) ints, and ignore the top three bytes completely. This works because when you cast an int to a byte it has exactly the effect of masking off the top three bytes. In my source code, the method
intArrayToByteArray
does this conversion, and is used before all file writes.
The crazy thing about this situation is that intArrayToByteArray
actually does not change the size of the data elements, only the type.
This is because JVMs actually manipulate all integers smaller
than 32 bits as if they were 32 bits. Computationally this makes sense
-- on a 32-bit CPU you don't gain anything by working in smaller chunks.
But it does mean that an array of bytes takes up exactly the same amount
of memory as an array of ints of the same dimension.
What all this means is that when we define a file header like this:
static final int header[] = new int[]
{
0x4d, 0x54, 0x64....
We don't lose anything in terms of storage over the situation we would
have if we were allowed to say:
static final unsigned byte header[] = new unsigned byte[]
{
0x4d, 0x54, 0x64....
But we do waste some CPU cycles casting blocks of data from int to
byte, when this operation does not, in fact, have any discernable effect
on any values. Oh well, that's Java for you.
The other complication to be aware of relates to endian-ness. The MIDI
file format is big-endian, but its integers can be between one and
four bytes long. If all integers were four bytes, we could rely only
the big-endianness of Java data storage (which is independent
of the machine architecture) to write out
integers directly. But since they aren't, there are places where we
have to do the math and split an integer into byte chains of various
sizes. The source code below does this where it is necessary. You'll
note that I've made some simplifications where it's unlikely that the
full range of the number will be needed. This is just to speed things
up on the Android platform, which isn't overwhelmingly fast at
Java math.
The MidiFile class
TheMidiFile class (see download link at the
end of this article) can be used as follows:
MidiFile mf = new MidiFile();
// Insert some notes
mf.noteOnOffNow (CROCHET, 60, 127) // 60=Middle C
// Etc...
mg.writeToFile ("somefile.mid");
That's all there is too it. The attached code demonstrates various
other ways in which notes can be inserted.
Limitations of the code, and possible improvements
I've tried to find the simplest, fastest way to write an uncomplicated MIDI file, which will work on the Android java platform. There are many limitations, some of which you'd almost certainly want to do something about in a serious application.- Maximum note length is a semibreve. For music that is made up of crotchets, and fractions and multiples of crotchets, I would recommend using constants CROTCHET, MINIM, rather than numbers. That way you're less likely to write a delta that is too long and which will break the player. Moreover, you only have to adjust the constants if you decide to change the timebase. Remember that, unless you change the code, the maximum delta is 127 (just under 2*SEMIBREVE). Dealing with longer deltas (which you'll need to with a faster timebase) is not hugely difficult, but it will make the files much larger, and considerably increase the complexity of the arithmetic.
- Fixed tempo of crotchet=60. Refer to the the definition
of
tempoEventto see where to change this. The tempo is a number of microseconds in 3-byte big-endian format. Some math will be required. - Only one controller channel. The channel number is specified as
the second four-bit quantity on each even message. So to send a
note-on to channel 1, you need to set the value 0x90 in
the
noteOnmethod to0x90+channel, and similarly for all the other event types. - The only performance events handled are note on/off and program change. But with a copy of the MIDI spec to hand, it wouldn't be hard to add other events.
- Data is buffered in memory. The Java class accumulates all MIDI events and writes them out to file in one go. In most cases this isn't a huge problem, because the kind of application I envisage won't be playing hours of music. But it would be more memory efficient to write out data in chunks and them free the memory. Of course, once the file was complete you'd have to go back and fill in the track data length.
Source download
Here is a Java soure code example that demonstrates three different ways in which theMidiFile class can be used:
to specify note on and note off events at explicit deltas, to
specify notes as durations, in the understanding that one follows the
other without rests, and to specify notes as an array of values
and durations, and let the method noteSequenceFixedVelocity
take care of the rests.
Have you posted something in response to this page?
Feel free to send a webmention
to notify me, giving the URL of the blog or page that refers to
this one.


