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();
}
}
|
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.
|