Sun Java Solaris Communities My SDN Account Join SDN
 
Technical Articles and Tips

Producing MIDI Sound and Saving and Reconstituting Swing Components

 
View this issue as simple text August 5, 2003    

Welcome to the Core Java Technologies Tech Tips, August 5, 2003. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).

This issue covers:

-Producing MIDI Sound
-Saving and Reconstituting Swing Components

These tips were developed using Java 2 SDK, Standard Edition, v 1.4.

This issue of the Core Java Technologies Tech Tips is written by Daniel H. Steinberg, Director of Java Offerings for Dim Sum Thinking, Inc, and editor-in-chief for java.net.

See the Subscribe/Unsubscribe note at the end of this newsletter to subscribe to Tech Tips that focus on technologies and products in other Java platforms.

.
.

PRODUCING MIDI SOUND

Adding audio cues to your Java desktop application can give it more of a polished feel and can dramatically improve usability. The Musical Instrument Digital Interface (MIDI) is a communication protocol for passing musical events between devices. A MIDI file contains audio commands rather than actual audio. Audio is a digital representation of sound, while MIDI represents the commands to a sound engine to recreate the sound. In this tech tip, you'll learn three ways to generate MIDI sound to augment your J2SE applications. Each of these techniques uses the javax.sound.midi package which has been part of the Java platform since J2SE 1.3.

In the first technique, you generate a sound by making a direct call to a MidiChannel object. This object represent a single MIDI channel, that is, a single channel for MIDI transmission. To start a note playing through the channel, you use the noteOn() method of the MidiChannel object. To stop a note playing, you use the noteOff() method.

The noteOn() method requires two ints. The first indicates the note being played. In the following program, SingleNoteChannel, the int 60 passed to the noteOn() method is the standard MIDI note number for middle C. An integer up or down corresponds to a half step. Twelve half steps comprise an octave.

The second parameter for the noteOn() method indicates the speed with which the key is struck. Although this parameter is often referred to as the velocity, you can think of it as a volume control. In the SingleNoteChannel program, the second parameter passed to the noteOn() method is 70. After you run SingleNoteChannel, experiment with other speeds by changing 70 to other numbers.

There are two signatures of the noteOff() method. One takes the same two parameters taken by noteOn(), and the other requires the number corresponding to the note being played.

Before you can play a note, there is a certain amount of setup to be done. This is demonstrated in the SingleNoteChannel constructor. In order to generate sound, you need a Synthesizer object. This object represents a collection of MidiChannels, usually one for each of the 16 channels prescribed by the MIDI 1.0 specification. The SingleNoteChannel constructor gets a handle to a Synthesizer using a factory method in the MidiSystem class. The constructor then calls the Synthesizer's open() method, and gets an array of the available MidiChannels by calling the getChannels() method. It then selects the first MidiChannel at index 0.

   import javax.sound.midi.MidiChannel;
   import javax.sound.midi.Synthesizer;
   import javax.sound.midi.MidiSystem;
   import javax.sound.midi.MidiUnavailableException;

   public class SingleNoteChannel {

      private MidiChannel channel;

      public SingleNoteChannel() {
        try {
          Synthesizer synth = 
                          MidiSystem.getSynthesizer();
          synth.open();
          channel = synth.getChannels()[0];
        } catch (MidiUnavailableException e) {
          e.printStackTrace();
        }
      }

      public void playNote(int note) {
        channel.noteOn(note, 70);
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        channel.noteOff(note, 70);
      }

      public static void main(String[] args) {
        new SingleNoteChannel().playNote(60);
      }
   }

The playNote() method in the SingleNoteChannel program starts playing a note with a call to the MidiChannel noteOn() method. The note continues to play while the thread sleeps for one second. Then a call to the MidiChannel noteOff() method ends the playing of the note.

You could use this first technique to associate certain notes with the pressing and releasing of buttons, keys, or other on-screen events.

Now let's look at a second technique to generate MIDI sound. In this approach, you use a Receiver object associated with a Synthesizer. Instead of getting a specific MidiChannel for a Synthesizer, you get a handle to a Receiver. You create MIDI messages of type ShortMessage, customize them, and then play them by calling the send() method of the Receiver object.

A ShortMessage object contains a MIDI message that has at most two data bytes. You use the setMessage() method to set the parameters of the MIDI message or to set message parameters for a channel message (this depends on the signature of the method) In the program below, SingleNoteSynthesizer, the signature of the setMessage() method sets message parameters for a channel message. This signature takes four ints. The first indicates the command being sent. The choices are NOTE_ON and NOTE_OFF. The second int identifies the target channel. To be consistent with the previous example, this parameter specifies index 0. For the NOTE_ON and NOTE_OFF commands the last two ints are the note identifier and velocity. In SingleNoteSynthesizer they are the same ints that were passed into the noteOn() and noteOff() methods in the SingleNoteChannel example.

   import javax.sound.midi.ShortMessage;
   import javax.sound.midi.InvalidMidiDataException;
   import javax.sound.midi.Receiver;
   import javax.sound.midi.Synthesizer;
   import javax.sound.midi.MidiSystem;
   import javax.sound.midi.MidiUnavailableException;

   public class SingleNoteSynthesizer {

      private ShortMessage message = 
                                    new ShortMessage();
      private Receiver receiver;

      private SingleNoteSynthesizer() {
        try {
          Synthesizer synth = 
                           MidiSystem.getSynthesizer();
          synth.open();
          receiver = synth.getReceiver();
        } catch (MidiUnavailableException e) {
          e.printStackTrace();
        }
      }
  
      public void playNote(int note) {
        setShortMessage(note, ShortMessage.NOTE_ON);
        receiver.send(message, -1);
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
       e.printStackTrace();
        }
        setShortMessage(note, ShortMessage.NOTE_OFF);
        receiver.send(message, -1);
      }

      private void setShortMessage(
                               int note, int onOrOff) {
        try {
          message.setMessage(onOrOff, 0, note, 70);
        } catch (InvalidMidiDataException e) {
          e.printStackTrace();
        }
      }

      public static void main(String[] args) {
        new SingleNoteSynthesizer().playNote(60);
      }
   }

So far you've seen examples that play a single note. One thing you might not have realized is that the examples played the note on a default instrument. But what about playing a note on a different instrument? In this third technique for generating MIDI sound you'll see how to do this. Let's start by examining what's available. Here's a method that generates a list of available instruments:

   public void listAvailableInstruments(){
      Instrument[] instrument =
                     synth.getAvailableInstruments();
      for (int i=0; i<instrument.length; i++){
        System.out.println(i + "   "
                          + instrument[i].getName());
      }
   }

If you call the method, you get a list whose first items look like this:

0   Piano
1   Bright Piano
2   Electric Grand
3   Honky Tonk Piano
4   Electric Piano 1
5   Electric Piano 2
6   Harpsichord
7   Clavinet
8   Celesta
9   Glockenspiel
10   Music Box
11   Vibraphone
12   Marimba
13   Xylophone
14   Tubular Bell
15   Dulcimer
16   Hammond Organ
17   Perc Organ
18   Rock Organ
19   Church Organ
20   Reed Organ
21   Accordion
22   Harmonica
23   Tango Accordion
24   Nylon Str Guitar
25   Steel String Guitar
26   Jazz Electric Gtr
27   Clean Guitar
28   Muted Guitar
29   Overdrive Guitar
30   Distortion Guitar

To illustrate the approach, let's enhance SingleNoteSynthesizer to play more than one note at a time using different instruments. If you examine the enhanced program, SingleNoteSynthesizer2, you will notice that the program still sleeps a Thread to measure time. This is really not robust enough for creating and playing music. In addition, if done in the AWT Event Dispatch thread, this will prevent your GUI from processing events or repainting. These issues will be addressed later in the tip using the final technique for generating MIDI sound.

As demonstrated in SingleNoteSynthesizer2, you change the instrument being played by calling the programChange() method in MidiChannel. You pass the method an int that corresponds to the instrument you want from the list you generated above. In this example, the int is 19, corresponding to a church organ.

Notice too that the code has also been refactored so that startNote() creates a message with the NOTE_ON command, and stopNote() creates one with the NOTE_OFF command. The playNote() method calls startNote(), sleeps for the number of milliseconds passed in as the duration, and then calls stopNote().

   import javax.sound.midi.ShortMessage;
   import javax.sound.midi.Synthesizer;
   import javax.sound.midi.Receiver;
   import javax.sound.midi.MidiSystem;
   import javax.sound.midi.MidiUnavailableException;
   import javax.sound.midi.InvalidMidiDataException;

   public class SingleNoteSynthesizer2 {
      private ShortMessage message = new ShortMessage();
      private Synthesizer synth;
      private Receiver receiver;

      public SingleNoteSynthesizer2() {
        try {
          synth = MidiSystem.getSynthesizer();
          synth.open();
          receiver = synth.getReceiver();
        } catch (MidiUnavailableException e) {
          e.printStackTrace();
        }
      }

      public void startNote(int note) {
        setShortMessage(ShortMessage.NOTE_ON, note);
        receiver.send(message, -1);
      }

      public void stopNote(int note) {
        setShortMessage(ShortMessage.NOTE_OFF, note);
        receiver.send(message, -1);
      }

      public void playNote(int note, int duration){
        startNote(note);
        try {
          Thread.sleep(duration);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        stopNote(note);
      }

      public void setInstrument(int instrument){
        synth.getChannels()[0].programChange(
                                           instrument);
      }

      private void setShortMessage(
                               int onOrOff, int note) {
        try {
          message.setMessage(onOrOff, 0, note, 70);
        } catch (InvalidMidiDataException e) {
          e.printStackTrace();
        }
      }

      public void playMajorChord(int baseNote){
        playNote(baseNote,1000);
        playNote(baseNote+4,1000);
        playNote(baseNote+7, 1000);
        startNote(baseNote);
        startNote(baseNote+4);
        playNote(baseNote+7,2000);
        stopNote(baseNote+4);
        stopNote(baseNote);
      }

      public static void main(String[] args) {
        SingleNoteSynthesizer2 synth
                        = new SingleNoteSynthesizer2();
        synth.setInstrument(19);
        synth.playMajorChord(60);
      }
   }

A major chord is constructed from a base note, the note four half steps above it, and the note three half spaces above that. In SingleNoteSynthesizer2, the playMajorChord() method first plays these notes, one at a time, and then plays them all at once. The timing is done using Threads. This is not the recommended approach when timing is important. In that case you should use a Sequencer.

A Sequencer object includes methods that give you more control (as compared to the previous approach) over the timing of MIDI events. As before, use a factory method from MidiSystem. This time obtain and open a Sequencer. This is shown in the next example, SequencerSound. Comparing this example to the preceding examples, you should notice some notable changes to the code. First the startNote() and stopNote() methods take an int that represents the point in time at which the message being created takes place. The createTrack() method creates a Sequence with four ticks for each quarter note. You can think of ticks as subdivisions. In other words, a measure of music in 4/4 time has four quarter notes or sixteen ticks.

The setShortMessage() method calls setMessage(), as before. Then the program creates a MidiEvent object based on this ShortMessage and associated with a specific tick. Finally the program adds a MidiEvent to the Track. To play the track, the startSequencer() method assigns the newly created Sequence to the Sequencer. The playback begins with a call to the start() method, and the tempo is set to sixty beats per minute (BPM) using the setTempoInBPM() method.

   import javax.sound.midi.Track;
   import javax.sound.midi.Sequencer;
   import javax.sound.midi.Sequence;
   import javax.sound.midi.MidiSystem;
   import javax.sound.midi.MidiUnavailableException;
   import javax.sound.midi.InvalidMidiDataException;
   import javax.sound.midi.ShortMessage;
   import javax.sound.midi.MidiEvent;

   public class SequencerSound {
      private Track track;
      private Sequencer sequencer;
      private Sequence sequence;

      public SequencerSound() {
        try {
          sequencer = MidiSystem.getSequencer();
          sequencer.open();
        } catch (MidiUnavailableException e) {
          e.printStackTrace();
        }
        createTrack();
        makeScale(20);
        startSequencer();
      }

      private void startSequencer() {
        try {
          sequencer.setSequence(sequence);
        } catch (InvalidMidiDataException e) {
          e.printStackTrace();
        }
        sequencer.start();
        sequencer.setTempoInBPM(60);
      }

      private void createTrack() {
        try {
          sequence = new Sequence(Sequence.PPQ, 4);
        } catch (InvalidMidiDataException e) {
          e.printStackTrace();
        }
        track = sequence.createTrack();
      }

      public void startNote(int note, int tick) {
        setShortMessage(
                     ShortMessage.NOTE_ON, note, tick);
      }

      public void stopNote(int note, int tick) {
        setShortMessage(
                    ShortMessage.NOTE_OFF, note, tick);
      }

      private void setShortMessage(
                    int onOrOff, int note, int tick) {
        ShortMessage message = new ShortMessage();
        try {
          message.setMessage(onOrOff, 0, note, 90);
          MidiEvent event = new MidiEvent(
                                       message, tick);
          track.add(event);
        } catch (InvalidMidiDataException e) {
          e.printStackTrace();
        }
      }

      public void makeScale(int baseNote) {
        for (int i = 0; i < 13; i++) {
          startNote(baseNote + i, i);
          stopNote(baseNote + i, i + 1);
          startNote(baseNote + i, 25 - i);
          stopNote(baseNote + i, 26 - i);
        }
      }

      public static void main(String[] args) {
        new SequencerSound();
      }
   }

This tip showed you how to produce and play back MIDI files. As a developer, you are able to hear the music because a soundbank is automatically installed along with the J2SE SDK. A soundbank represents a set of instruments, and is needed to synthesize sound. (There's a lot more to the soundbank concept, and it will be covered in a later tip.) Non-developers might have to install a soundbank separately to hear what you have created. When installing a JRE for J2SE 1.4.1 or later, users have to opt-in to include the soundbank in the installation.

For more information on saving and playing back MIDI files, consult the documentation for javax.sound.midi. You'll find more information on the Java Sound API home page.

.
.

SAVING AND RECONSTITUTING SWING COMPONENTS

Before the J2SE 1.4 release, all Swing components have included the advisory that "A future release of Swing will provide support for long term persistence." This has been replaced with the notice that "As of 1.4, support for long term storage of all JavaBeans has been added to the java.beans package."

The XMLEncoder class in the java.beans package allows you to persist a JavaBean in an XML file. Among other things, this provides a way to save Swing components. After you save a Swing component, you can reconstitute the component with the XMLDecoder class in the the java.beans package.

The following program, FrameCreator, creates and customizes a JFrame. The main() method in the program instantiates an XMLEncoder object, and then uses it to store an XML representation of the JFrame in the file Frame.xml.

   import javax.swing.*;
   import java.awt.*;
   import java.awt.event.ActionListener;
   import java.beans.XMLEncoder;
   import java.beans.EventHandler;
   import java.io.BufferedOutputStream;
   import java.io.FileOutputStream;

   public class FrameCreator {
      public static JFrame createFrame() {
        JFrame jFrame = new JFrame();
        jFrame.setTitle("My Frame");
        jFrame.setSize(200, 100);
        JPanel jPanel = new JPanel();
        jFrame.getContentPane().add(jPanel);
        Controller controller = new Controller();
        JButton button1 = new JButton("Hello, world");
        button1.addActionListener(
          (ActionListener) EventHandler.create(
            ActionListener.class, controller,
            "helloWorld"));
        JButton button2 = new JButton(
            "Goodbye, cruel world");
        button2.setBackground(Color.RED);
        button2.addActionListener(
          (ActionListener) EventHandler.create(
            ActionListener.class, controller,
            "exit"));
        jPanel.add(button1);
        jPanel.add(button2);
        jFrame.setVisible(true);
        return jFrame;
      }

      public static void main(String[] args)
                                    throws Exception {
        XMLEncoder encoder = new XMLEncoder(
            new BufferedOutputStream(
                new FileOutputStream("Frame.xml")));
        encoder.writeObject(createFrame());
        encoder.close();
      }
   }

Note that FrameCreator calls some methods defined in a non-GUI class called Controller.java. Here's the Controller class:

   public class Controller {
        public void helloWorld() {
            System.out.println("Hello, world");
        }

        public void exit() {
            System.exit(0);
        }
   }

Here is the contents of Frame.xml. You can easily identify the properties that were set in the FrameCreator class. Notice that the dimensions of the JFrame are saved. Also saved in the file is information about the JPanel stored inside the JFrame, and the JPanel's constituent components: two JButtons linked to an instance of the Controller class.

   <?xml version="1.0" encoding="UTF-8"?>
   <java version="1.4.1_01" class="java.beans.XMLDecoder">
     <object class="javax.swing.JFrame">
      <void property="size">
       <object class="java.awt.Dimension">
        <int>200</int>
        <int>100</int>
       </object>
      </void>
      <void property="contentPane">
       <void method="add">
        <object class="javax.swing.JPanel">
         <void method="add">
          <object class="javax.swing.JButton">
           <string>Hello, world</string>
           <void method="addActionListener">
            <object class="java.beans.EventHandler"
                    method="create">
             <class>java.awt.event.ActionListener</class>
             <object id="Controller0" class="Controller"/>
             <string>helloWorld</string>
            </object>
           </void>
          </object>
         </void>
         <void method="add">
          <object class="javax.swing.JButton">
           <string>Goodbye, cruel world</string>
           <void property="background">
            <object class="java.awt.Color">
             <int>255</int>
             <int>0</int>
             <int>0</int>
             <int>255</int>
            </object>
           </void>
           <void method="addActionListener">
            <object class="java.beans.EventHandler"
                    method="create">
             <class>java.awt.event.ActionListener</class>
             <object idref="Controller0"/>
             <string>exit</string>
            </object>
           </void>
          </object>
         </void>
        </object>
       </void>
      </void>
      <void property="name">
       <string>frame0</string>
      </void>
      <void property="title">
       <string>My Frame</string>
      </void>
      <void property="visible">
       <boolean>true</boolean>
      </void>
     </object>
   </java>

It takes surprisingly little to reconstitute the configured JFrame from the file Frame.xml. The following program, FrameRecreator, instantiates an XMLDecoder that reads from the file Frame.xml. When you run FrameRecreator, you should see the JFrame that was created and configured just as it was in FrameCreator.

   import java.beans.XMLDecoder;
   import java.io.BufferedInputStream;
   import java.io.FileInputStream;

   public class FrameRecreator {

      public static void main(String[] args)
                                    throws Exception {
        XMLDecoder decoder = new XMLDecoder(
            new BufferedInputStream(
                new FileInputStream("Frame.xml")));
        decoder.readObject();
        decoder.close();
      }
   }

Frame Recreator

You've seen how to use the classes XMLEncoder and XMLDecoder to save and restore JavaBeans. However, XMLEncoder and XMLDecoder are much more general than this example implies. XMLEncoder is preconfigured to save all primitive data types, dates, strings, arrays, lists, hashmaps, and many other classes in the J2SE SDK that are not JavaBeans. You can also configure the XMEncoder to save your own Java classes, even if they aren't JavaBeans. For details on how to do this, and for information about other features of these technologies, see the following articles in the Swing Connection:

You can add XMLEncoder to your set of persistence techniques, such as the Preferences API discussed in the tip titled "Using the Preferences API" in the July 15 issue of the Core Java Technologies Tech Tips.

.
.

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html


Comments? Send your feedback on the Core Java Technologies Tech Tips to: http://developers.sun.com/contact/feedback.jsp?category=sdn

Subscribe to other Java developer Tech Tips:

- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).

To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".


ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html


Copyright 2003 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.


This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html


Java, J2SE, J2EE, J2ME, and all Java-based marks are trademarks or registered trademarks (http://www.sun.com/suntrademarks/) of Sun Microsystems, Inc. in the United States and other countries.