Reliably playing a short sound in Java
I am tyring to write some Java code that basically just plays a short .wav file - with 'short' I mean a fraction of a second. (The file I use is at /usr/share/sounds/generic.wav for those of you using Ubuntu.)
The problem is, I can't seem to figure out how to play that sample reliably, i.e., in all my attempts, I can get my program to play the sound in 4 out of 5 times or so, but never 100%.
This is what has worked best so far as a stand-alone program:
File soundFile = new File("/usr/share/sounds/generic.wav");
Clip clip = AudioSystem.getClip();
AudioInputStream inputStream = AudioSystem.getAudioInputStream(soundFile);
clip.open(inputStream);
clip.start();
(Note that the code doesn't even call clip.stop()) But even with that one, if I run it a couple of times in a row, sooner or later there will be a run without any sound being played, but no Exceptions either.
Variations I've tried:
1) Loading the audio file into a byte array and passing that to clip.open
2) Attaching a LineListener to the clip to wait for STOP even开发者_开发百科ts
plus a couple of random try-outs, but so far I haven't managed to create code that works every time.
I'm also aware of the following bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4434125 but I'm using Java 1.6 and the report claims that things should be fine with Java 1.5 or later.
Any ideas? Is it PulseAudio?
I've had great luck with the BASS Audio Library.
It's written natively, so it breaks write-once, run-anywhere, but it will work on Windows, OS/X, and Linux, which is anywhere enough for my needs.
I suspect now that the reason my test program failed was a timing issue. Either I attempted playing the short sound before the samples were fully loaded, or the program terminated too quickly. The reason for this suspicion is that if I change the above code slightly like so:
File soundFile = new File("/usr/share/sounds/generic.wav");
Clip clip = AudioSystem.getClip();
AudioInputStream inputStream = AudioSystem.getAudioInputStream(soundFile);
clip.open(inputStream);
while (System.in.read() == '\n') {
clip.stop();
clip.setFramePosition(0);
clip.start();
}
then the short sound gets played correctly every time I hit enter.
The most straightforward approach here is probably to just get the exact clipLength in milliseconds (rounding up) and use that to sleep the thread playing the sound for the duration. Make sure you use synchronized to avoid IllegalMonitorStateExceptions.
synchronized(clip){
clip.start();
try{
double clipLength = audioParams.getFrameLength() /
audioParams.getFormat().getFrameRate();
clip.wait(java.lang.Math.round(clipLength +.5)*1000);
} catch (InterruptedException e) {
System.out.println( e.getMessage() );
}
c.stop();
}
I have had good luck using the following code in an application (even though it uses the Applet's newAudioClip() method):
AudioClip clip;
File fileClip = new File("/usr/share/sounds/generic.wav");
URL url = null;
try
{
URI uri = fileClip.toURI();
url = uri.toURL();
clip = Applet.newAudioClip(url);
}
catch (MalformedURLException e){}
clip.play();
I got this method from: Starting Out with Java: From Control Structures Through Objects, 4th Edition by Tony Gaddis, Addison Wesley, ISBN-13 978-0-13-608020-6
How quickly are you rerunning your calls to play the Clip? I was messing around with making a "wind chime" that had six bell .wav files loaded as Clips, and had some concurrency issues with calls to play the sounds failing. I came up with a scheme that actually made a new Clip on a new thread with each trigger rather than trying to rerun existing Clips and that worked, but I think it is inherently inefficient. (If one is going to the trouble of making new threads anyway, then maybe you might as well run streams and avoid the overhead of loading the entire before playing. I have to test this theory.) BTW: I was able to run close to 100 threads at a time, if I recall correctly. The separate thread approach allows the wav files to complete and "overlap" rather than cut each other off. Fun to watch on JProfiler!
There are commands that stop a sound and move the starting point back to the beginning. Are you doing that? That might allow reuse in situations where the Clip is called before it has finished.
Here's some code I have been using. I edited it because there was a lot of other stuff in here you dont need, so sorry if its a little messy.
Call
Wav player = new Wav("sound.wav");
player.playAudio(player.getBytes());
import java.applet.Applet;
import java.applet.AudioClip;
import java.net.URISyntaxException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.io.*;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import javax.sound.sampled.*;
/**
* This class handles the reading, writing, and playing of wav files. It is
* also capable of converting the file to its raw byte [] form.
*
* based on code by Evan Merz
*/
public class Wav {
ByteArrayOutputStream byteArrayOutputStream;
AudioFormat audioFormat;
TargetDataLine targetDataLine;
AudioInputStream audioInputStream;
SourceDataLine sourceDataLine;
float frequency = 8000.0F; //8000,11025,16000,22050,44100
int samplesize = 16;
private String myPath;
private long myChunkSize;
private long mySubChunk1Size;
private int myFormat;
private long myChannels;
private long mySampleRate;
private long myByteRate;
private int myBlockAlign;
private int myBitsPerSample;
private long myDataSize;
// I made this public so that you can toss whatever you want in here
// maybe a recorded buffer, maybe just whatever you want
public byte[] myData;
public Wav()
{
myPath = "";
}
// constructor takes a wav path
public Wav(String tmpPath) {
myPath = tmpPath;
}
// get set for the Path property
public String getPath()
{
return myPath;
}
public void setPath(String newPath)
{
myPath = newPath;
}
// read a wav file into this class
public boolean read() {
DataInputStream inFile = null;
myData = null;
byte[] tmpLong = new byte[4];
byte[] tmpInt = new byte[2];
try {
inFile = new DataInputStream(new FileInputStream(myPath));
//System.out.println("Reading wav file...\n"); // for debugging only
String chunkID = "" + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte();
inFile.read(tmpLong); // read the ChunkSize
myChunkSize = byteArrayToLong(tmpLong);
String format = "" + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte();
// print what we've read so far
//System.out.println("chunkID:" + chunkID + " chunk1Size:" + myChunkSize + " format:" + format); // for debugging only
String subChunk1ID = "" + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte();
inFile.read(tmpLong); // read the SubChunk1Size
mySubChunk1Size = byteArrayToLong(tmpLong);
inFile.read(tmpInt); // read the audio format. This should be 1 for PCM
myFormat = byteArrayToInt(tmpInt);
inFile.read(tmpInt); // read the # of channels (1 or 2)
myChannels = byteArrayToInt(tmpInt);
inFile.read(tmpLong); // read the samplerate
mySampleRate = byteArrayToLong(tmpLong);
inFile.read(tmpLong); // read the byterate
myByteRate = byteArrayToLong(tmpLong);
inFile.read(tmpInt); // read the blockalign
myBlockAlign = byteArrayToInt(tmpInt);
inFile.read(tmpInt); // read the bitspersample
myBitsPerSample = byteArrayToInt(tmpInt);
// print what we've read so far
//System.out.println("SubChunk1ID:" + subChunk1ID + " SubChunk1Size:" + mySubChunk1Size + " AudioFormat:" + myFormat + " Channels:" + myChannels + " SampleRate:" + mySampleRate);
// read the data chunk header - reading this IS necessary, because not all wav files will have the data chunk here - for now, we're just assuming that the data chunk is here
String dataChunkID = "" + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte() + (char) inFile.readByte();
inFile.read(tmpLong); // read the size of the data
myDataSize = byteArrayToLong(tmpLong);
// read the data chunk
myData = new byte[(int) myDataSize];
inFile.read(myData);
// close the input stream
inFile.close();
} catch (Exception e) {
return false;
}
return true; // this should probably be something more descriptive
}
// return a printable summary of the wav file
public String getSummary() {
//String newline = System.getProperty("line.separator");
String newline = "
";
String summary = "Format: " + myFormat + newline + "Channels: " + myChannels + newline + "SampleRate: " + mySampleRate + newline + "ByteRate: " + myByteRate + newline + "BlockAlign: " + myBlockAlign + newline + "BitsPerSample: " + myBitsPerSample + newline + "DataSize: " + myDataSize + "";
return summary;
}
public byte[] getBytes() {
read();
return myData;
}
/**
* Plays back audio stored in the byte array using an audio format given by
* freq, sample rate, ect.
* @param data The byte array to play
*/
public void playAudio(byte[] data) {
try {
byte audioData[] = data;
//Get an input stream on the byte array containing the data
InputStream byteArrayInputStream = new ByteArrayInputStream(audioData);
AudioFormat audioFormat = getAudioFormat();
audioInputStream = new AudioInputStream(byteArrayInputStream, audioFormat, audioData.length / audioFormat.getFrameSize());
DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, audioFormat);
sourceDataLine = (SourceDataLine) AudioSystem.getLine(dataLineInfo);
sourceDataLine.open(audioFormat);
sourceDataLine.start();
//Create a thread to play back the data and start it running. It will run \
//until all the data has been played back.
Thread playThread = new Thread(new PlayThread());
playThread.start();
} catch (Exception e) {
System.out.println(e);
}
}
/**
* This method creates and returns an AudioFormat object for a given set
* of format parameters. If these parameters don't work well for
* you, try some of the other allowable parameter values, which
* are shown in comments following the declarations.
* @return
*/
private AudioFormat getAudioFormat() {
float sampleRate = frequency;
//8000,11025,16000,22050,44100
int sampleSizeInBits = samplesize;
//8,16
int channels = 1;
//1,2
boolean signed = true;
//true,false
boolean bigEndian = false;
//true,false
//return new AudioFormat( AudioFormat.Encoding.PCM_SIGNED, 8000.0f, 8, 1, 1,
//8000.0f, false );
return new AudioFormat(sampleRate, sampleSizeInBits, channels, signed, bigEndian);
}
// ===========================
// CONVERT BYTES TO JAVA TYPES
// ===========================
// these two routines convert a byte array to a unsigned short
public static int byteArrayToInt(byte[] b) {
int start = 0;
int low = b[start] & 0xff;
int high = b[start + 1] & 0xff;
return (int) (high > 8) & 0x000000FF);
b[2] = (byte) ((i >> 16) & 0x000000FF);
b[3] = (byte) ((i >> 24) & 0x000000FF);
return b;
}
// convert a short to a byte array
public static byte[] shortToByteArray(short data) {
return new byte[]{(byte) (data & 0xff), (byte) ((data >>> 8) & 0xff)};
}
/**
* Inner class to play back the data that was saved
*/
class PlayThread extends Thread {
byte tempBuffer[] = new byte[10000];
public void run() {
try {
int cnt;
//Keep looping until the input
// read method returns -1 for
// empty stream.
while ((cnt = audioInputStream.read(tempBuffer, 0, tempBuffer.length)) != -1) {
if (cnt > 0) {
//Write data to the internal
// buffer of the data line
// where it will be delivered
// to the speaker.
sourceDataLine.write(tempBuffer, 0, cnt);
}
}
//Block and wait for internal
// buffer of the data line to
// empty.
sourceDataLine.drain();
sourceDataLine.close();
} catch (Exception e) {
System.out.println(e);
System.exit(0);
}
}
}
}
精彩评论