|
Tech Tips Archive
March 19, 2002
WELCOME to the Java Developer Connection (JDC) Tech Tips, March 19, 2002. This issue covers:
These tips were developed using Java 2 SDK, Standard Edition, v 1.4.
This issue of the JDC Tech Tips is written by John Zukowski,
president of JZ Ventures, Inc. (http://www.jzventures.com).
CAPTURING AUDIO WITH THE JAVA SOUND API
The Java Sound API has been a part of the standard libraries of
the Java 2 Platform since the 1.3 release. Found in the
javax.sound.sampled package, the Java Sound API provides support
for playing and capturing audio. In addition, the library offers
a software-based audio mixer and MIDI (Musical Instrument Digital
Interface) device access. In this tip, you'll learn how to
capture audio through the Java Sound API and play it back.
The javax.sound.sampled package consists of eight interfaces,
twelve top-level classes, twelve inner classes, and two
exceptions. To record and play audio, you only need to deal with
a total of seven parts of the package.
Let's examine recording first. The basic recording process is as
follows:
Describe the audio format in which you want to record the data.
This includes specifying the sampling rate and the number of
channels (mono versus stereo) for the audio. You specify these
properties using the aptly named AudioFormat class. There are two
constructors for creating an AudioFormat object:
AudioFormat(AudioFormat.Encoding encoding,
float sampleRate, int sampleSizeInBits,
int channels, int frameSize, float frameRate,
boolean bigEndian)
AudioFormat(float sampleRate, int sampleSizeInBits,
int channels, boolean signed, boolean bigEndian)
|
The first constructor lets you explicitly set the audio format
encoding, while the latter uses a default. The available
encodings are ALAW, PCM_SIGNED, PCM_UNSIGNED, and ULAW. The
default encoding used for the second constructor is PCM. Here
is an example that uses the second constructor to create
an AudioFormat object for single channel recording in 8 kHz
format:
float sampleRate = 8000;
int sampleSizeInBits = 8;
int channels = 1;
boolean signed = true;
boolean bigEndian = true;
AudioFormat format = new AudioFormat(sampleRate,
sampleSizeInBits, channels, signed, bigEndian);
|
After you describe the audio format, you need to get a DataLine.
This interface represents an audio feed from which you can
capture the audio. You use a subinterface of DataLine to do the
actual capturing. The subinterface is called TargetDataLine. To
get the TargetDataLine, you ask the AudioSystem. However when
you do that, you need to specify information about the line. You
make the specification in the form of a DataLine.Info object. In
particular, you need to create a DataLine.Info object that is
specific to the DataLine type and audio format. Here are some
lines of source that get the TargetDataLine.
DataLine.Info info = new DataLine.Info(
TargetDataLine.class, format);
TargetDataLine line = (TargetDataLine)
AudioSystem.getLine(info);
If the TargetDataLine is unavailable, a LineUnavailableException
is thrown.
At this point you have your input source. You can think of the
TargetDataLine like an input stream. However, it requires some
setup before you can read form it. Setup in this case means first
opening the line using the open() method, and then initializing
the line using the start() method:
line.open(format);
line.start();
Your data line is ready, so you can start recording from it as
shown in the following lines of code. Here you save a captured
audio stream to a byte array for later playing. You could also
save the audio stream to a file. Notice that you have to manage
when to stop outside the read-loop construct.
int bufferSize = (int)format.getSampleRate() *
format.getFrameSize();
byte buffer[] = new byte[bufferSize];
out = new ByteArrayOutputStream();
while (externalTrigger) {
int count = line.read(buffer, 0, buffer.length);
if (count > 0) {
out.write(buffer, 0, count);
}
}
out.close();
|
Now let's examine playing audio. There are two key differences in
playing audio as compared to recording audio. First, when you
play audio, the bytes come from an AudioInputStream instead of a
TargetDataLine. Second, you write to a SourceDataLine instead of
into a ByteArrayOutputStream. Besides that, the process is the
same.
To get the AudioInputStream, you need to convert the
ByteArrayOutputStream into the source of the AudioInputStream.
The AudioInputStream constructor requires the bytes from the output
stream, the audio format encoding used, and the number of sample
frames:
byte audio[] = out.toByteArray();
InputStream input = new ByteArrayInputStream(audio);
AudioInputStream ais = new AudioInputStream(input,
format, audio.length / format.getFrameSize());
Getting the DataLine is similar to the way you get it for audio
recording, but for playing audio, you need to fetch a
SourceDataLine instead of a TargetDataLine:
DataLine.Info info = new DataLine.Info(
SourceDataLine.class, format);
SourceDataLine line =
(SourceDataLine)AudioSystem.getLine(info);
Setup for the line is identical to the setup for audio recording:
line.open(format);
line.start();
The last step is to play the audio as shown below. Notice that
this step is similar to the last step in recording. However, here
you read from the buffer and write to the data line. There is
also an added drain operation that works like a flush on an
output stream.
int bufferSize = (int) format.getSampleRate()
* format.getFrameSize();
byte buffer[] = new byte[bufferSize];
int count;
while ((count =
ais.read(buffer, 0, buffer.length)) != -1) {
if (count > 0) {
line.write(buffer, 0, count);
}
}
line.drain();
line.close();
|
The following program puts these steps together to demonstrate
using the Java Sound the API to record and play audio. The
program also presents a GUI. Press the Capture button to start
recording the audio, the Stop button to stop recording, and
the Play button to play back the audio.
Note: Depending on the audio support your platform provides, you
might need to change the format returned by the getFormat method
in the program. If you don't know what format is supported by
your platform, download the demo program from the Java Sound Demo
page, and run it. Click on the CapturePlayback tab, and find a
set of format settings that work for you. You can load an audio
file from the audio subdirectory, and then try various settings
on the left until something works. Use those settings in the
creation of the AudioFormat returned by getFormat().
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import javax.sound.sampled.*;
public class Capture extends JFrame {
protected boolean running;
ByteArrayOutputStream out;
public Capture() {
super("Capture Sound Demo");
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container content = getContentPane();
final JButton capture = new JButton("Capture");
final JButton stop = new JButton("Stop");
final JButton play = new JButton("Play");
capture.setEnabled(true);
stop.setEnabled(false);
play.setEnabled(false);
ActionListener captureListener =
new ActionListener() {
public void actionPerformed(ActionEvent e) {
capture.setEnabled(false);
stop.setEnabled(true);
play.setEnabled(false);
captureAudio();
}
};
capture.addActionListener(captureListener);
content.add(capture, BorderLayout.NORTH);
ActionListener stopListener =
new ActionListener() {
public void actionPerformed(ActionEvent e) {
capture.setEnabled(true);
stop.setEnabled(false);
play.setEnabled(true);
running = false;
}
};
stop.addActionListener(stopListener);
content.add(stop, BorderLayout.CENTER);
ActionListener playListener =
new ActionListener() {
public void actionPerformed(ActionEvent e) {
playAudio();
}
};
play.addActionListener(playListener);
content.add(play, BorderLayout.SOUTH);
}
private void captureAudio() {
try {
final AudioFormat format = getFormat();
DataLine.Info info = new DataLine.Info(
TargetDataLine.class, format);
final TargetDataLine line = (TargetDataLine)
AudioSystem.getLine(info);
line.open(format);
line.start();
Runnable runner = new Runnable() {
int bufferSize = (int)format.getSampleRate()
* format.getFrameSize();
byte buffer[] = new byte[bufferSize];
public void run() {
out = new ByteArrayOutputStream();
running = true;
try {
while (running) {
int count =
line.read(buffer, 0, buffer.length);
if (count > 0) {
out.write(buffer, 0, count);
}
}
out.close();
} catch (IOException e) {
System.err.println("I/O problems: " + e);
System.exit(-1);
}
}
};
Thread captureThread = new Thread(runner);
captureThread.start();
} catch (LineUnavailableException e) {
System.err.println("Line unavailable: " + e);
System.exit(-2);
}
}
private void playAudio() {
try {
byte audio[] = out.toByteArray();
InputStream input =
new ByteArrayInputStream(audio);
final AudioFormat format = getFormat();
final AudioInputStream ais =
new AudioInputStream(input, format,
audio.length / format.getFrameSize());
DataLine.Info info = new DataLine.Info(
SourceDataLine.class, format);
final SourceDataLine line = (SourceDataLine)
AudioSystem.getLine(info);
line.open(format);
line.start();
Runnable runner = new Runnable() {
int bufferSize = (int) format.getSampleRate()
* format.getFrameSize();
byte buffer[] = new byte[bufferSize];
public void run() {
try {
int count;
while ((count = ais.read(
buffer, 0, buffer.length)) != -1) {
if (count > 0) {
line.write(buffer, 0, count);
}
}
line.drain();
line.close();
} catch (IOException e) {
System.err.println("I/O problems: " + e);
System.exit(-3);
}
}
};
Thread playThread = new Thread(runner);
playThread.start();
} catch (LineUnavailableException e) {
System.err.println("Line unavailable: " + e);
System.exit(-4);
}
}
private AudioFormat getFormat() {
float sampleRate = 8000;
int sampleSizeInBits = 8;
int channels = 1;
boolean signed = true;
boolean bigEndian = true;
return new AudioFormat(sampleRate,
sampleSizeInBits, channels, signed, bigEndian);
}
public static void main(String args[]) {
JFrame frame = new Capture();
frame.pack();
frame.show();
}
}
|
You can find a more complete example of using the Java Sound API
at the Java Sound Demo page. That example also shows an oscilloscope of the sound wave as it
is playing back.
VALIDATING DESERIALIZED OBJECTS
The serialization mechanism of the standard I/O libraries
provides for reading and writing objects to byte streams. This
saves the state of those objects so that you can later recreate
them. The basic serialization process was explained in a prior Tech Tip devoted to this topic.
The basic object serialization process is sufficient for most
needs. However, sometimes you need additional serialization
capabilities. Fortunately, the serialization mechanism provides
features that go beyond the basics. One of these additional
features pertains to object validation. Object validation is
important because serialization allows an object state to be
stored outside the runtime environment. This leads to the
question how can you be sure that the state you get back still
represents a valid object? So you need to ensure that a
object is in the proper state.
The way to ensure that a deserialized object is in the proper
state is to register an ObjectInputValidation interface with the
ObjectInputStream class. You do this through the
registerValidation method of ObjectInputStream. The first
parameter to the registerValidation method (the validator)
identifies the object that receives the validation callback.
The second parameter to the registerValidation method helps you
order the validators if there are multiple validators present.
public class TheClass implements Serializable {
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
ObjectInputValidation validator = ...;
ois.registerValidation(validator, 0);
ois.defaultReadObject();
}
}
|
This technique allows you to check the object state when an
object is completely initialized (that is after it's read).
Being completely initialized is important when the object being
validated is subclassed. The validation happens after the
subclass is initialized, not when the object is read using the
readObject method of the parent.
The ObjectInputValidation interface includes a single method,
validateObject, that throws an InvalidObjectException if the
validation fails. You don't have to catch this exception in the
readObject method. Instead, the framework that calls readObject
passes the exception along when validation fails.
public void validateObject()
throws InvalidObjectException {
// Data fields are initialized
if (! checkData()) {
throw new InvalidObjectException(
"Invalid data");
}
}
|
The following example demonstrates the object validation mechanism.
It also shows when the validation happens. The example provides a
serializable parent and child classes. The parent class has a
registered validator. The validation is not complicated, it's
simply a check to see if the elements of a collection are of a
specific type. The program serializes both a parent and child
object without a validation error. Then the program tries to do
the task again. The program fails in this second try due to a
validation error.
import java.io.*;
import java.util.*;
import javax.swing.*;
public class Parent implements Serializable,
ObjectInputValidation {
List list;
Class type;
static class Child extends Parent {
public Child(List data, Class datatype) {
super(data, datatype);
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
System.out.println("Reading Child");
ois.defaultReadObject();
System.out.println("Done Reading Child");
}
private void writeObject(ObjectOutputStream oos)
throws IOException {
System.out.println("Writing Child");
oos.defaultWriteObject();
}
}
public Parent(List data, Class datatype) {
list = data;
type = datatype;
}
private void readObject(ObjectInputStream ois)
throws ClassNotFoundException, IOException {
System.out.println("Reading Parent");
ois.registerValidation(this, 0);
ois.defaultReadObject();
System.out.println("Done Reading Parent");
}
private void writeObject(ObjectOutputStream oos)
throws IOException {
System.out.println("Writing");
oos.defaultWriteObject();
}
public void validateObject()
throws InvalidObjectException {
System.out.println("Validating");
Iterator it = list.iterator();
while (it.hasNext()) {
if (! type.isInstance(it.next())) {
throw new InvalidObjectException(
"Invalid type in collection");
}
}
}
public static void main(String args[])
throws Exception {
List aList = new ArrayList();
aList.add(new JButton("Hello, "));
aList.add(new JLabel("World"));
Parent p = new Parent(aList,
javax.swing.JComponent.class);
try {
saveAndRestore(p, "parent.ser");
} catch (InvalidObjectException e) {
System.err.println(
"Unable to restore parent/1");
}
System.out.println("----\n");
Child c = new Child(aList,
javax.swing.JComponent.class);
try {
saveAndRestore(c, "child.ser");
} catch (InvalidObjectException e) {
System.err.println(
"Unable to restore child/1");
}
System.out.println("----\n");
aList.add("Problem");
try {
saveAndRestore(p, "parent2.ser");
} catch (InvalidObjectException e) {
System.err.println(
"Unable to restore parent/2");
}
System.out.println("----\n");
try {
saveAndRestore(c, "child2.ser");
} catch (InvalidObjectException e) {
System.err.println(
"Unable to restore child/2");
}
}
private static Object saveAndRestore(
Parent p, String filename)
throws IOException, ClassNotFoundException {
FileOutputStream fos =
new FileOutputStream(filename);
ObjectOutputStream oos =
new ObjectOutputStream(fos);
oos.writeObject(p);
oos.close();
FileInputStream fis =
new FileInputStream(filename);
ObjectInputStream ois =
new ObjectInputStream(fis);
Object obj = ois.readObject();
ois.close();
return obj;
}
}
|
For more information on object serialization capabilities,
including the new features in the J2SE 1.4 release, see the Object Serialization section of the Java 2 SDK, Standard Edition Documentation.
IMPORTANT: Please read our Terms of Use and Privacy policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html
FEEDBACK
Comments? Send your feedback on the JDC Tech Tips to:
jdc-webmaster@sun.com
SUBSCRIBE/UNSUBSCRIBE
- To subscribe, go to the subscriptions 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 JDC Tech Tips archives at:
http://java.sun.com/jdc/TechTips/index.html
- COPYRIGHT
Copyright 2002 Sun Microsystems, Inc. All rights reserved.
901 San Antonio Road, Palo Alto, California 94303 USA.
This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html
JDC Tech Tips
March 19, 2002
Sun, Sun Microsystems, Java, Java Developer Connection, and J2SE are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.
|