MIDI in Java
MIDI in Java
MIDI in Java
with java
MIDI
by Mike Gorman
&
www.JavaDevelopersJournal.com
HOME
J2EE
J2SE
J2ME
he Java Sound API, first introduced in J2SE 1.3, includes the package javax.sound.midi, which contains everything you need to be able to send and receive messages to and from any MIDI device visible to your operating system.
42
November 2003
The Java Sound Programmer Guide and the Java Sound Demo, both available for download from Sun, are excellent references that illustrate all the nuts and bolts of sending and receiving messages. This article provides a brief overview of working with the MIDI and sampled audio primitives of the Java Sound API, and then explores using those primitives to construct a basic multi-track MIDI/audio sequencer in Java.
With the API its easy to create your own sequencer. First we have to be able to record a single track and play it back in the exact same timing it was originally played in. Moreover, the performing artist (thats you) will want to have a four-bar metronome count off prior to recording start, then, to keep perfect time, youll need to continue the metronome until the user clicks stop. Of course, the metronome sounds should not be part of the performance when its played back. You can have the computer emit a system beep for your metronome, but I prefer to listen to the hi-hat of a drumkit on the keyboard. A lot of sequencers use MIDI channel 10 (i.e., track 10) as a drumkit, but you can choose any one you like. A tempo of 120 beats per minute (bpm) means 2 beats/second or 1 tick of your metronome every 500ms. As you may have guessed, youll need one thread playing the ticks of your metronome (on Channel 10) while you record any MidiMessages you receive (via your Receiver implementation) on Channel 1. (Note that I am referring to the channels/tracks from the musicians perspective. The channel references in the API are zero-based.) Listing 5 shows what your metronome thread might look like. Playing the metronome is simple enough, but you need to figure out a way to play it through just one time, then begin recording MidiMessages as they arrive at your receiver (discussed later). How you do that will be left for you to decide.
J2EE
J2SE
J2ME
Recording MIDI
How exactly do you record MidiMessages? There are basically two strategies: you can try to take note of what time each message arrives, or you can use the included timestamp of each message. In either strategy, your implementation of the Receiver interface will create an ArrayList and add each MidiMessage it receives to the ArrayList. Of course, youll need to make sure you record only MidiMessages for the duration immediately following the four-bar Metronome count off until the user clicks stop. Your first strategy might be to use System.currentTimeMillis() to take note of the current system time (in ms) at which each MidiMessage arrives. Youll need to know this when you play back these messages. The general idea is to play back the messages using a thread, thats sleeping between messages, according to the relative time they originally arrived. In my experience, the system clock was not reliable enough to deliver rock-solid timings during playback. Youll know what I mean if you try this strategy when you listen to the playback of messages based on the system clock. The other strategy is to use the embedded timestamp that accompanies each MidiMessage. This timestamp is expressed in microseconds based on the time you first opened the MidiDevice. Unfortunately, by the time the four-bar metronome count off ends, its difficult to say when the first message should be played back. That is you cant assume that the first message that arrives should be played back at time zero. Perhaps the musicians first note is played halfway through the first measure. Since the MidiDevice was opened long before your metronome began playing, its difficult to determine from the timestamp alone how much time your playback thread should wait until it sends the very first message. Of course, all messages after that are easy, since you can just calculate the time to wait in between each message based on the relative differences of the messages timestamps. The best solution I came up with was to just take note (by way of System.currentTimeMillis()) of when recording actually begins (that is, after the four-bar metronome count off), and then take note of when the first MidiMessage arrives. Then, during playback, the playback thread merely needs to wait the www.JavaDevelopersJournal.com
HOME
calculated delay time before playing back the first message. Thereafter, it can simply use the relative differences between the MidiMessage timestamps for all subsequent messages. It may surprise you to learn that what you think of as a chord (or several chords across multiple tracks) struck simultaneously is actually played back one note at a time, sent serially as a stream of MidiMessages, one at a time. You have to remember that the playback loop playing back the messages is so fast that the human ear will not be able to discern the difference between the original three notes struck simultaneously and three notes played 1 ms apart. You should now be able able to record and play back a single MIDI track at 120 bpm. If, when it plays back, it sounds just like you played it, youre halfway there. The next step is to be able record additional MIDI tracks while playing back previously recorded tracks.
code is getting cluttered up with complex thread synchronization all over the place, and it becomes harder and harder to manage and still achieve rock solid timing. What I found to be easier to manage and virtually guaranteed to stay in time was to collect all MidiMessages, regardless of track (channel), put them into a single ArrayList, sort them all based on their timestamp, and then play them all back using a single playback thread.
J2ME
J2SE
Ultimately, any recorded digital audio comes down to samples. A sample is a measurement at a point in time of what you might picture as the audio waveform. The standard CD sampling rate is to take 44,100 measurements, or samples, each second. Each sample may be 8 bits, 16 bits, or more. There are a variety of sample formats in use today, and the Java Sound API supports about everything youll encounter. Some useful constants for recording CD quality sound are:
AudioFormat.Encoding encoding = AudioFormat.Encoding.PCM_SIGNED; int rate = 44100; int sampleSize = 16; int channels = 1; boolean bigEndian = true;
HOME
J2EE
Next, route all incoming MIDI messages to the keyboard, playing them back on the track the user thinks he is recording. For example, you may receive all your MIDI messages with the channel 1 byte set. If the user thinks she is recording track 2, then for each MIDI message received, in addition to recording it (by storing it in track 2s message ArrayList), change the channel byte to 2 and retransmit them back to the keyboard (see Listing 6).
Before you can begin recording, however, youll need to obtain a TargetDataLine. The Java Sound API models its sampling API in terms of lines. A line may be a microphone input, a previously recorded sample, the computers line out or speaker, or any type of input or output. To facilitate the playback of multiple samples at the same time, the interface Mixer is provided, which is itself a type of line. Lines may have controls that parallel what youd find in a real mixer gain, pan, volume, reverb, equalization, etc. Like the MidiDevices returned from the MidiSystem, the class AudioSystem serves as your gateway into finding out and obtaining whatever Lines and Controls are installed and available to you. In general, the first step to recording an audio track is to obtain a TargetDataLine suitable for recording audio in the format requested, in this case an AudioFormat that is a single 16-bit channel recording 44,100 samples/second (see Listing 7). As you may have suspected, youll need a separate thread to capture the incoming sample data. Using the TargetDataLine and OutputStream created previously, youll want to create a loop that reads a chunk of bytes at a time from the TargetDataLine, writing them out to the OutputStream until theres nothing left to read or until the user clicks stop (see Listing 8). At this point, your ByteArrayOutputStream contains a ton of bytes. The average 3:30 minute song will require 9.3MB worth of samples for just a single mono track! FileOutputwww.JavaDevelopersJournal.com
Stream might be a better choice if youre going to be recording lengthy samples and memory becomes scarce. Of course, recording the sample is just half of the story. Now we have to play it back. J2ME
References
Open source MIDI and audio projects: Audio Development System: http://sourceforge.net/projects/adsystem jMusic: http://sourceforge.net/projects/jmusic Sound Grid: http://sourceforge.net/projects/soundgrid
API References
Java Sound Programmer Guide: http://java.sun.com/j2se/1.4.1/docs/guide/sound/pro grammer_guide/contents.html Java Sound Demo: http://java.sun.com/products/javamedia/sound/samples/JavaSoundDemo/
J2SE
MIDI Specification
Official MIDI Specification: www.midi.org Online MIDI Specification (unofficial): www.borg.com/~jglatt/tech/midispec.htm
Miscellaneous
Bug ID 4773012: RFE: Implement a new stand-alone sequencer: http://developer.java.sun.com/developer/ bugParade/bugs/4773012.html Bug ID 4783745: Sequencer cannot access external MIDI devices: http://developer.java.sun.com/developer/ bugParade/bugs/4783745.html
Listing 1: Displaying the MIDI devices available
1MidiDevice.Info[] info = 2 MidiSystem.getMidiDeviceInfo(); 3 4 for (int i=0; i < info.length; i++) { 5 log(i + ") " + info[i]); 6 log("Name: " + info[i].getName()); 7 log("Description: " + 8 info[i].getDescription()); 9 10 MidiDevice device = 11 MidiSystem.getMidiDevice(info[i]); 12 log("Device: " + device); 13}
HOME
J2EE
Mike Gorman is a senior software architect for J.D. Edwards, a PeopleSoft company, concentrating on J2EE distributed transaction systems. Mike has been coding in Java since 1997. In his spare time, Mike plays with MIDI, Swing, Web services, and JDO.
[email protected]
48 November 2003
www.JavaDevelopersJournal.com
J2ME
J2EE
J2SE
HOME
50
November 2003
www.JavaDevelopersJournal.com
6 new AudioInputStream( 7new ByteArrayInputStream(data), 8 getAudioFormat(), data.length / 9 frameSizeInBytes); 10 11try { 12 audioInputStream.mark(2000000000); 13 audioInputStream.reset(); 14} catch (IOException e) { 15 e.printStackTrace(); 16 return; 17} 18 19long duration = (long) 20 ((audioInputStream.getFrameLength() * 21 1000) / getAudioFormat().getFrameRate());
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57}
int bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes; byte[] data = new byte[ bufferLengthInBytes]; // start the source data line sourceLine.start(); // main playback loop while (isPlaying()) { // rewind at start of each loop getAudioInputStream().reset(); while (true) { int numBytesRead = getAudioInputStream().read( data); if (numBytesRead == -1 || isPlaying() == false) { break; } int numBytesRemaining = numBytesRead; while (numBytesRemaining > 0) { numBytesRemaining -= sourceLine.write(data, 0, numBytesRemaining); } } // Weve reached the end of the // stream. Let the data play out, // then stop and close the line. sourceLine.drain(); } sourceLine.stop(); sourceLine.close(); catch (LineUnavailableException e) { e.printStackTrace(); catch (IOException e) { e.printStackTrace(); catch (InterruptedException e) { e.printStackTrace(); catch (JStudioException e) { e.printStackTrace();
} } } } }
www.JavaDevelopersJournal.com
November 2003
51