package bs.sound;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;
import bs.util.LoopingByteInputStream;
import bs.util.ThreadPool;
/**
* The SoundManager class manages sound playback. The SoundManager is a
* ThreadPool, with each thread playing back one sound at a time. This allows
* the SoundManager to easily limit the number of simultaneous sounds being
* played.
* <p>
* Possible ideas to extend this class:
* <ul>
* <li>add a setMasterVolume() method, which uses Controls to set the volume for
* each line.
* <li>don't play a sound if more than, say, 500ms has passed since the request
* to play
* </ul>
*/
public class SoundManager extends ThreadPool {
private static final int MAX_SIMULTANEOUS = 32;
private AudioFormat playbackFormat;
private ThreadLocal localLine;
private ThreadLocal localBuffer;
private Object pausedLock;
private boolean paused;
/**
* Creates a new SoundManager using the maximum number of simultaneous
* sounds.
*/
public SoundManager(AudioFormat playbackFormat) {
this(playbackFormat, getMaxSimultaneousSounds(playbackFormat));
}
/**
* Creates a new SoundManager with the specified maximum number of
* simultaneous sounds.
*/
public SoundManager(AudioFormat playbackFormat, int maxSimultaneousSounds) {
super("SoundManager", Math.min(maxSimultaneousSounds, getMaxSimultaneousSounds(playbackFormat)));
this.playbackFormat = playbackFormat;
localLine = new ThreadLocal();
localBuffer = new ThreadLocal();
pausedLock = new Object();
// notify threads in pool it's ok to start
synchronized (this) {
notifyAll();
}
}
/**
* Gets the maximum number of simultaneous sounds with the specified
* AudioFormat that the default mixer can play.
*/
public static int getMaxSimultaneousSounds(AudioFormat playbackFormat) {
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, playbackFormat);
Mixer mixer = AudioSystem.getMixer(null);
final int max = mixer.getMaxLines(lineInfo);
return max != AudioSystem.NOT_SPECIFIED ? max : MAX_SIMULTANEOUS;
}
/**
* Does any clean up before closing.
*/
protected void cleanUp() {
// signal to unpause
setPaused(false);
// close the mixer (stops any running sounds)
Mixer mixer = AudioSystem.getMixer(null);
if (mixer.isOpen()) {
mixer.close();
}
}
public void close() {
cleanUp();
super.close();
}
public void join() {
cleanUp();
super.join();
}
/**
* Sets the paused state. Sounds may not pause immediately.
*/
public void setPaused(boolean paused) {
if (this.paused != paused) {
synchronized (pausedLock) {
this.paused = paused;
if (!paused) {
// restart sounds
pausedLock.notifyAll();
}
}
}
}
/**
* Returns the paused state.
*/
public boolean isPaused() {
return paused;
}
/**
* Loads a Sound from the file system. Returns null if an error occurs.
*/
public Sound getSound(String filename) {
return getSound(getAudioInputStream(filename));
}
/**
* Loads a Sound from an input stream. Returns null if an error occurs.
*/
public Sound getSound(InputStream is) {
return getSound(getAudioInputStream(is));
}
/**
* Loads a Sound from an AudioInputStream.
*/
public Sound getSound(AudioInputStream audioStream) {
if (audioStream == null) {
return null;
}
// get the number of bytes to read
int length = (int) (audioStream.getFrameLength() * audioStream.getFormat().getFrameSize());
// read the entire stream
byte[] samples = new byte[length];
DataInputStream is = new DataInputStream(audioStream);
try {
is.readFully(samples);
is.close();
} catch (IOException ex) {
ex.printStackTrace();
}
// return the samples
return new Sound(samples);
}
/**
* Creates an AudioInputStream from a sound from the file system.
*/
public AudioInputStream getAudioInputStream(String filename) {
try {
return getAudioInputStream(new FileInputStream(filename));
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
/**
* Creates an AudioInputStream from a sound from an input stream
*/
public AudioInputStream getAudioInputStream(InputStream is) {
try {
if (!is.markSupported()) {
is = new BufferedInputStream(is);
}
// open the source stream
AudioInputStream source = AudioSystem.getAudioInputStream(is);
// convert to playback format
return AudioSystem.getAudioInputStream(playbackFormat, source);
} catch (UnsupportedAudioFileException ex) {
ex.printStackTrace();
} catch (IOException ex) {
ex.printStackTrace();
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
}
return null;
}
/**
* Plays a sound. This method returns immediately.
*/
public InputStream play(Sound sound) {
return play(sound, null, false);
}
/**
* Plays a sound with an optional SoundFilter, and optionally looping. This
* method returns immediately.
*/
public InputStream play(Sound sound, SoundFilter filter, boolean loop) {
InputStream is;
if (sound != null) {
if (loop) {
is = new LoopingByteInputStream(sound.getSamples());
} else {
is = new ByteArrayInputStream(sound.getSamples());
}
return play(is, filter);
}
return null;
}
/**
* Plays a sound from an InputStream. This method returns immediately.
*/
public InputStream play(InputStream is) {
return play(is, null);
}
/**
* Plays a sound from an InputStream with an optional sound filter. This
* method returns immediately.
*/
public InputStream play(InputStream is, SoundFilter filter) {
if (is != null) {
if (filter != null) {
is = new FilteredSoundStream(is, filter);
}
runTask(new SoundPlayer(is));
}
return is;
}
/**
* Signals that a PooledThread has started. Creates the Thread's line and
* buffer.
*/
protected void threadStarted() {
// wait for the SoundManager constructor to finish
synchronized (this) {
try {
wait();
} catch (InterruptedException ex) {
}
}
// use a short, 100ms (1/10th sec) buffer for filters that
// change in real-time
int bufferSize = playbackFormat.getFrameSize() * Math.round(playbackFormat.getSampleRate() / 10);
// create, open, and start the line
SourceDataLine line;
DataLine.Info lineInfo = new DataLine.Info(SourceDataLine.class, playbackFormat);
try {
line = (SourceDataLine) AudioSystem.getLine(lineInfo);
line.open(playbackFormat, bufferSize);
} catch (LineUnavailableException ex) {
// the line is unavailable - signal to end this thread
Thread.currentThread().interrupt();
return;
}
line.start();
// create the buffer
byte[] buffer = new byte[bufferSize];
// set this thread's locals
localLine.set(line);
localBuffer.set(buffer);
}
/**
* Signals that a PooledThread has stopped. Drains and closes the Thread's
* Line.
*/
protected void threadStopped() {
SourceDataLine line = (SourceDataLine) localLine.get();
if (line != null) {
line.drain();
line.close();
}
}
/**
* The SoundPlayer class is a task for the PooledThreads to run. It receives
* the threads's Line and byte buffer from the ThreadLocal variables and
* plays a sound from an InputStream.
* <p>
* This class only works when called from a PooledThread.
*/
protected class SoundPlayer implements Runnable {
private InputStream source;
public SoundPlayer(InputStream source) {
this.source = source;
}
public void run() {
// get line and buffer from ThreadLocals
SourceDataLine line = (SourceDataLine) localLine.get();
byte[] buffer = (byte[]) localBuffer.get();
if (line == null || buffer == null) {
// the line is unavailable
return;
}
// copy data to the line
try {
int numBytesRead = 0;
while (numBytesRead != -1) {
// if paused, wait until unpaused
synchronized (pausedLock) {
if (paused) {
try {
pausedLock.wait();
} catch (InterruptedException ex) {
return;
}
}
}
// copy data
numBytesRead = source.read(buffer, 0, buffer.length);
if (numBytesRead != -1) {
line.write(buffer, 0, numBytesRead);
}
}
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}