* This class wasn't listed in the book ;) * * @see SoundManager * @see Sound * @see SoundFilter */ public class SoundManagerTest extends GameCore implements ActionListener { public static void main(String[] args) { new SoundManagerTest().run(); } // uncompressed, 44100Hz, 16-bit, mono, signed, little-endian private static final AudioFormat PLAYBACK_FORMAT = new AudioFormat(44100, 16, 1, true, false); private static final int MANY_SOUNDS_COUNT = SoundManager .getMaxSimultaneousSounds(PLAYBACK_FORMAT); private static final int DRUM_TRACK = 1; private static final String EXIT = "Exit"; private static final String PAUSE = "Pause"; private static final String PLAY_MUSIC = "Play Music"; private static final String MUSIC_DRUMS = "Toggle Drums"; private static final String PLAY_SOUND = "Play Sound"; private static final String PLAY_ECHO_SOUND = "Play Echoed Sound"; private static final String PLAY_LOOPING_SOUND = "Play Looping Sound"; private static final String PLAY_MANY_SOUNDS = "Play " + MANY_SOUNDS_COUNT + " Sounds"; private SoundManager soundManager; private MidiPlayer midiPlayer; private Sequence music; private Sound boop; private Sound bzz; private InputStream lastloopingSound; public void init() { super.init(); initSounds(); initUI(); } /** * Loads sounds and music. */ public void initSounds() { midiPlayer = new MidiPlayer(); soundManager = new SoundManager(PLAYBACK_FORMAT); music = midiPlayer.getSequence("../sounds/music.midi"); boop = soundManager.getSound("../sounds/boop.wav"); bzz = soundManager.getSound("../sounds/fly-bzz.wav"); } /** * Creates the UI, which is a row of buttons. */ public void initUI() { // make sure Swing components don't paint themselves NullRepaintManager.install(); JFrame frame = super.screen.getFullScreenWindow(); Container contentPane = frame.getContentPane(); contentPane.setLayout(new FlowLayout()); contentPane.add(createButton(PAUSE, true)); contentPane.add(createButton(PLAY_MUSIC, true)); contentPane.add(createButton(MUSIC_DRUMS, false)); contentPane.add(createButton(PLAY_SOUND, false)); contentPane.add(createButton(PLAY_ECHO_SOUND, false)); contentPane.add(createButton(PLAY_LOOPING_SOUND, true)); contentPane.add(createButton(PLAY_MANY_SOUNDS, false)); contentPane.add(createButton(EXIT, false)); // explicitly layout components (needed on some systems) frame.validate(); } /** * Draws all Swing components */ public void draw(Graphics2D g) { JFrame frame = super.screen.getFullScreenWindow(); frame.getLayeredPane().paintComponents(g); } /** * Creates a button (either JButton or JToggleButton). */ public AbstractButton createButton(String name, boolean canToggle) { AbstractButton button; if (canToggle) { button = new JToggleButton(name); } else { button = new JButton(name); } button.addActionListener(this); button.setIgnoreRepaint(true); button.setFocusable(false); return button; } /** * Performs actions when a button is pressed. */ public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); AbstractButton button = (AbstractButton) e.getSource(); if (command == EXIT) { midiPlayer.close(); soundManager.close(); stop(); } else if (command == PAUSE) { // pause the sound soundManager.setPaused(button.isSelected()); midiPlayer.setPaused(button.isSelected()); } else if (command == PLAY_MUSIC) { // toggle music on or off if (button.isSelected()) { midiPlayer.play(music, true); } else { midiPlayer.stop(); } } else if (command == MUSIC_DRUMS) { // toggle drums on or off Sequencer sequencer = midiPlayer.getSequencer(); if (sequencer != null) { boolean mute = sequencer.getTrackMute(DRUM_TRACK); sequencer.setTrackMute(DRUM_TRACK, !mute); } } else if (command == PLAY_SOUND) { // play a normal sound soundManager.play(boop); } else if (command == PLAY_ECHO_SOUND) { // play a sound with an echo EchoFilter filter = new EchoFilter(11025, .6f); soundManager.play(boop, filter, false); } else if (command == PLAY_LOOPING_SOUND) { // play or stop the looping sound if (button.isSelected()) { lastloopingSound = soundManager.play(bzz, null, true); } else if (lastloopingSound != null) { try { lastloopingSound.close(); } catch (IOException ex) { } lastloopingSound = null; } } else if (command == PLAY_MANY_SOUNDS) { // play several sounds at once, to test the system for (int i = 0; i < MANY_SOUNDS_COUNT; i++) { soundManager.play(boop); } } } } /** * 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. *
* Possible ideas to extend this class: *
* 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(); } } } } class MidiPlayer implements MetaEventListener { // Midi meta event public static final int END_OF_TRACK_MESSAGE = 47; private Sequencer sequencer; private boolean loop; private boolean paused; /** * Creates a new MidiPlayer object. */ public MidiPlayer() { try { sequencer = MidiSystem.getSequencer(); sequencer.open(); sequencer.addMetaEventListener(this); } catch (MidiUnavailableException ex) { sequencer = null; } } /** * Loads a sequence from the file system. Returns null if an error occurs. */ public Sequence getSequence(String filename) { try { return getSequence(new FileInputStream(filename)); } catch (IOException ex) { ex.printStackTrace(); return null; } } /** * Loads a sequence from an input stream. Returns null if an error occurs. */ public Sequence getSequence(InputStream is) { try { if (!is.markSupported()) { is = new BufferedInputStream(is); } Sequence s = MidiSystem.getSequence(is); is.close(); return s; } catch (InvalidMidiDataException ex) { ex.printStackTrace(); return null; } catch (IOException ex) { ex.printStackTrace(); return null; } } /** * Plays a sequence, optionally looping. This method returns immediately. * The sequence is not played if it is invalid. */ public void play(Sequence sequence, boolean loop) { if (sequencer != null && sequence != null && sequencer.isOpen()) { try { sequencer.setSequence(sequence); sequencer.start(); this.loop = loop; } catch (InvalidMidiDataException ex) { ex.printStackTrace(); } } } /** * This method is called by the sound system when a meta event occurs. In * this case, when the end-of-track meta event is received, the sequence is * restarted if looping is on. */ public void meta(MetaMessage event) { if (event.getType() == END_OF_TRACK_MESSAGE) { if (sequencer != null && sequencer.isOpen() && loop) { sequencer.start(); } } } /** * Stops the sequencer and resets its position to 0. */ public void stop() { if (sequencer != null && sequencer.isOpen()) { sequencer.stop(); sequencer.setMicrosecondPosition(0); } } /** * Closes the sequencer. */ public void close() { if (sequencer != null && sequencer.isOpen()) { sequencer.close(); } } /** * Gets the sequencer. */ public Sequencer getSequencer() { return sequencer; } /** * Sets the paused state. Music may not immediately pause. */ public void setPaused(boolean paused) { if (this.paused != paused && sequencer != null && sequencer.isOpen()) { this.paused = paused; if (paused) { sequencer.stop(); } else { sequencer.start(); } } } /** * Returns the paused state. */ public boolean isPaused() { return paused; } } /** * Simple abstract class used for testing. Subclasses should implement the * draw() method. */ abstract class GameCore { protected static final int FONT_SIZE = 24; private static final DisplayMode POSSIBLE_MODES[] = { new DisplayMode(800, 600, 32, 0), new DisplayMode(800, 600, 24, 0), new DisplayMode(800, 600, 16, 0), new DisplayMode(640, 480, 32, 0), new DisplayMode(640, 480, 24, 0), new DisplayMode(640, 480, 16, 0) }; private boolean isRunning; protected ScreenManager screen; /** * Signals the game loop that it's time to quit */ public void stop() { isRunning = false; } /** * Calls init() and gameLoop() */ public void run() { try { init(); gameLoop(); } finally { screen.restoreScreen(); } } /** * Sets full screen mode and initiates and objects. */ public void init() { screen = new ScreenManager(); DisplayMode displayMode = screen .findFirstCompatibleMode(POSSIBLE_MODES); screen.setFullScreen(displayMode); Window window = screen.getFullScreenWindow(); window.setFont(new Font("Dialog", Font.PLAIN, FONT_SIZE)); window.setBackground(Color.blue); window.setForeground(Color.white); isRunning = true; } public Image loadImage(String fileName) { return new ImageIcon(fileName).getImage(); } /** * Runs through the game loop until stop() is called. */ public void gameLoop() { long startTime = System.currentTimeMillis(); long currTime = startTime; while (isRunning) { long elapsedTime = System.currentTimeMillis() - currTime; currTime += elapsedTime; // update update(elapsedTime); // draw the screen Graphics2D g = screen.getGraphics(); draw(g); g.dispose(); screen.update(); // take a nap try { Thread.sleep(20); } catch (InterruptedException ex) { } } } /** * Updates the state of the game/animation based on the amount of elapsed * time that has passed. */ public void update(long elapsedTime) { // do nothing } /** * Draws to the screen. Subclasses must override this method. */ public abstract void draw(Graphics2D g); } /** * The NullRepaintManager is a RepaintManager that doesn't do any repainting. * Useful when all the rendering is done manually by the application. */ class NullRepaintManager extends RepaintManager { /** * Installs the NullRepaintManager. */ public static void install() { RepaintManager repaintManager = new NullRepaintManager(); repaintManager.setDoubleBufferingEnabled(false); RepaintManager.setCurrentManager(repaintManager); } public void addInvalidComponent(JComponent c) { // do nothing } public void addDirtyRegion(JComponent c, int x, int y, int w, int h) { // do nothing } public void markCompletelyDirty(JComponent c) { // do nothing } public void paintDirtyRegions() { // do nothing } } /** * The ScreenManager class manages initializing and displaying full screen * graphics modes. */ class ScreenManager { private GraphicsDevice device; /** * Creates a new ScreenManager object. */ public ScreenManager() { GraphicsEnvironment environment = GraphicsEnvironment .getLocalGraphicsEnvironment(); device = environment.getDefaultScreenDevice(); } /** * Returns a list of compatible display modes for the default device on the * system. */ public DisplayMode[] getCompatibleDisplayModes() { return device.getDisplayModes(); } /** * Returns the first compatible mode in a list of modes. Returns null if no * modes are compatible. */ public DisplayMode findFirstCompatibleMode(DisplayMode modes[]) { DisplayMode goodModes[] = device.getDisplayModes(); for (int i = 0; i < modes.length; i++) { for (int j = 0; j < goodModes.length; j++) { if (displayModesMatch(modes[i], goodModes[j])) { return modes[i]; } } } return null; } /** * Returns the current display mode. */ public DisplayMode getCurrentDisplayMode() { return device.getDisplayMode(); } /** * Determines if two display modes "match". Two display modes match if they * have the same resolution, bit depth, and refresh rate. The bit depth is * ignored if one of the modes has a bit depth of * DisplayMode.BIT_DEPTH_MULTI. Likewise, the refresh rate is ignored if one * of the modes has a refresh rate of DisplayMode.REFRESH_RATE_UNKNOWN. */ public boolean displayModesMatch(DisplayMode mode1, DisplayMode mode2) { if (mode1.getWidth() != mode2.getWidth() || mode1.getHeight() != mode2.getHeight()) { return false; } if (mode1.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI && mode2.getBitDepth() != DisplayMode.BIT_DEPTH_MULTI && mode1.getBitDepth() != mode2.getBitDepth()) { return false; } if (mode1.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN && mode2.getRefreshRate() != DisplayMode.REFRESH_RATE_UNKNOWN && mode1.getRefreshRate() != mode2.getRefreshRate()) { return false; } return true; } /** * Enters full screen mode and changes the display mode. If the specified * display mode is null or not compatible with this device, or if the * display mode cannot be changed on this system, the current display mode * is used. *
* The display uses a BufferStrategy with 2 buffers. */ public void setFullScreen(DisplayMode displayMode) { final JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setUndecorated(true); frame.setIgnoreRepaint(true); frame.setResizable(false); device.setFullScreenWindow(frame); if (displayMode != null && device.isDisplayChangeSupported()) { try { device.setDisplayMode(displayMode); } catch (IllegalArgumentException ex) { } // fix for mac os x frame.setSize(displayMode.getWidth(), displayMode.getHeight()); } // avoid potential deadlock in 1.4.1_02 try { EventQueue.invokeAndWait(new Runnable() { public void run() { frame.createBufferStrategy(2); } }); } catch (InterruptedException ex) { // ignore } catch (InvocationTargetException ex) { // ignore } } /** * Gets the graphics context for the display. The ScreenManager uses double * buffering, so applications must call update() to show any graphics drawn. *
* The application must dispose of the graphics object. */ public Graphics2D getGraphics() { Window window = device.getFullScreenWindow(); if (window != null) { BufferStrategy strategy = window.getBufferStrategy(); return (Graphics2D) strategy.getDrawGraphics(); } else { return null; } } /** * Updates the display. */ public void update() { Window window = device.getFullScreenWindow(); if (window != null) { BufferStrategy strategy = window.getBufferStrategy(); if (!strategy.contentsLost()) { strategy.show(); } } // Sync the display on some systems. // (on Linux, this fixes event queue problems) Toolkit.getDefaultToolkit().sync(); } /** * Returns the window currently used in full screen mode. Returns null if * the device is not in full screen mode. */ public JFrame getFullScreenWindow() { return (JFrame) device.getFullScreenWindow(); } /** * Returns the width of the window currently used in full screen mode. * Returns 0 if the device is not in full screen mode. */ public int getWidth() { Window window = device.getFullScreenWindow(); if (window != null) { return window.getWidth(); } else { return 0; } } /** * Returns the height of the window currently used in full screen mode. * Returns 0 if the device is not in full screen mode. */ public int getHeight() { Window window = device.getFullScreenWindow(); if (window != null) { return window.getHeight(); } else { return 0; } } /** * Restores the screen's display mode. */ public void restoreScreen() { Window window = device.getFullScreenWindow(); if (window != null) { window.dispose(); } device.setFullScreenWindow(null); } /** * Creates an image compatible with the current display. */ public BufferedImage createCompatibleImage(int w, int h, int transparancy) { Window window = device.getFullScreenWindow(); if (window != null) { GraphicsConfiguration gc = window.getGraphicsConfiguration(); return gc.createCompatibleImage(w, h, transparancy); } return null; } } /** * The EchoFilter class is a SoundFilter that emulates an echo. * * @see FilteredSoundStream */ class EchoFilter extends SoundFilter { private short[] delayBuffer; private int delayBufferPos; private float decay; /** * Creates an EchoFilter with the specified number of delay samples and the * specified decay rate. *
* The number of delay samples specifies how long before the echo is * initially heard. For a 1 second echo with mono, 44100Hz sound, use 44100 * delay samples. *
* The decay value is how much the echo has decayed from the source. A decay * value of .5 means the echo heard is half as loud as the source. */ public EchoFilter(int numDelaySamples, float decay) { delayBuffer = new short[numDelaySamples]; this.decay = decay; } /** * Gets the remaining size, in bytes, of samples that this filter can echo * after the sound is done playing. Ensures that the sound will have decayed * to below 1% of maximum volume (amplitude). */ public int getRemainingSize() { float finalDecay = 0.01f; // derived from Math.pow(decay,x) <= finalDecay int numRemainingBuffers = (int) Math.ceil(Math.log(finalDecay) / Math.log(decay)); int bufferSize = delayBuffer.length * 2; return bufferSize * numRemainingBuffers; } /** * Clears this EchoFilter's internal delay buffer. */ public void reset() { for (int i = 0; i < delayBuffer.length; i++) { delayBuffer[i] = 0; } delayBufferPos = 0; } /** * Filters the sound samples to add an echo. The samples played are added to * the sound in the delay buffer multipied by the decay rate. The result is * then stored in the delay buffer, so multiple echoes are heard. */ public void filter(byte[] samples, int offset, int length) { for (int i = offset; i < offset + length; i += 2) { // update the sample short oldSample = getSample(samples, i); short newSample = (short) (oldSample + decay * delayBuffer[delayBufferPos]); setSample(samples, i, newSample); // update the delay buffer delayBuffer[delayBufferPos] = newSample; delayBufferPos++; if (delayBufferPos == delayBuffer.length) { delayBufferPos = 0; } } } } /** * A abstract class designed to filter sound samples. Since SoundFilters may use * internal buffering of samples, a new SoundFilter object should be created for * every sound played. However, SoundFilters can be reused after they are * finished by called the reset() method. *
* Assumes all samples are 16-bit, signed, little-endian format. * * @see FilteredSoundStream */ abstract class SoundFilter { /** * Resets this SoundFilter. Does nothing by default. */ public void reset() { // do nothing } /** * Gets the remaining size, in bytes, that this filter plays after the sound * is finished. An example would be an echo that plays longer than it's * original sound. This method returns 0 by default. */ public int getRemainingSize() { return 0; } /** * Filters an array of samples. Samples should be in 16-bit, signed, * little-endian format. */ public void filter(byte[] samples) { filter(samples, 0, samples.length); } /** * Filters an array of samples. Samples should be in 16-bit, signed, * little-endian format. This method should be implemented by subclasses. */ public abstract void filter(byte[] samples, int offset, int length); /** * Convenience method for getting a 16-bit sample from a byte array. Samples * should be in 16-bit, signed, little-endian format. */ public static short getSample(byte[] buffer, int position) { return (short) (((buffer[position + 1] & 0xff) << 8) | (buffer[position] & 0xff)); } /** * Convenience method for setting a 16-bit sample in a byte array. Samples * should be in 16-bit, signed, little-endian format. */ public static void setSample(byte[] buffer, int position, short sample) { buffer[position] = (byte) (sample & 0xff); buffer[position + 1] = (byte) ((sample >> 8) & 0xff); } } /** * A thread pool is a group of a limited number of threads that are used to * execute tasks. */ class ThreadPool extends ThreadGroup { private boolean isAlive; private LinkedList taskQueue; private int threadID; private static int threadPoolID; /** * Creates a new ThreadPool. * * @param numThreads * The number of threads in the pool. */ public ThreadPool(int numThreads) { super("ThreadPool-" + (threadPoolID++)); setDaemon(true); isAlive = true; taskQueue = new LinkedList(); for (int i = 0; i < numThreads; i++) { new PooledThread().start(); } } /** * Requests a new task to run. This method returns immediately, and the task * executes on the next available idle thread in this ThreadPool. *
* Tasks start execution in the order they are received. * * @param task * The task to run. If null, no action is taken. * @throws IllegalStateException * if this ThreadPool is already closed. */ public synchronized void runTask(Runnable task) { if (!isAlive) { throw new IllegalStateException(); } if (task != null) { taskQueue.add(task); notify(); } } protected synchronized Runnable getTask() throws InterruptedException { while (taskQueue.size() == 0) { if (!isAlive) { return null; } wait(); } return (Runnable) taskQueue.removeFirst(); } /** * Closes this ThreadPool and returns immediately. All threads are stopped, * and any waiting tasks are not executed. Once a ThreadPool is closed, no * more tasks can be run on this ThreadPool. */ public synchronized void close() { if (isAlive) { isAlive = false; taskQueue.clear(); interrupt(); } } /** * Closes this ThreadPool and waits for all running threads to finish. Any * waiting tasks are executed. */ public void join() { // notify all waiting threads that this ThreadPool is no // longer alive synchronized (this) { isAlive = false; notifyAll(); } // wait for all threads to finish Thread[] threads = new Thread[activeCount()]; int count = enumerate(threads); for (int i = 0; i < count; i++) { try { threads[i].join(); } catch (InterruptedException ex) { } } } /** * Signals that a PooledThread has started. This method does nothing by * default; subclasses should override to do any thread-specific startup * tasks. */ protected void threadStarted() { // do nothing } /** * Signals that a PooledThread has stopped. This method does nothing by * default; subclasses should override to do any thread-specific cleanup * tasks. */ protected void threadStopped() { // do nothing } /** * A PooledThread is a Thread in a ThreadPool group, designed to run tasks * (Runnables). */ private class PooledThread extends Thread { public PooledThread() { super(ThreadPool.this, "PooledThread-" + (threadID++)); } public void run() { // signal that this thread has started threadStarted(); while (!isInterrupted()) { // get a task to run Runnable task = null; try { task = getTask(); } catch (InterruptedException ex) { } // if getTask() returned null or was interrupted, // close this thread. if (task == null) { break; } // run the task, and eat any exceptions it throws try { task.run(); } catch (Throwable t) { uncaughtException(this, t); } } // signal that this thread has stopped threadStopped(); } } } /** * The Sound class is a container for sound samples. The sound samples are * format-agnostic and are stored as a byte array. */ class Sound { private byte[] samples; /** * Create a new Sound object with the specified byte array. The array is not * copied. */ public Sound(byte[] samples) { this.samples = samples; } /** * Returns this Sound's objects samples as a byte array. */ public byte[] getSamples() { return samples; } } /** * The LoopingByteInputStream is a ByteArrayInputStream that loops indefinitly. * The looping stops when the close() method is called. *
* Possible ideas to extend this class: *
length
bytes from the array. If the end of the array
* is reached, the reading starts over from the beginning of the array.
* Returns -1 if the array has been closed.
*/
public int read(byte[] buffer, int offset, int length) {
if (closed) {
return -1;
}
int totalBytesRead = 0;
while (totalBytesRead < length) {
int numBytesRead = super.read(buffer, offset + totalBytesRead,
length - totalBytesRead);
if (numBytesRead > 0) {
totalBytesRead += numBytesRead;
} else {
reset();
}
}
return totalBytesRead;
}
/**
* Closes the stream. Future calls to the read() methods will return 1.
*/
public void close() throws IOException {
super.close();
closed = true;
}
}
/**
* The FilteredSoundStream class is a FilterInputStream that applies a
* SoundFilter to the underlying input stream.
*
* @see SoundFilter
*/
class FilteredSoundStream extends FilterInputStream {
private static final int REMAINING_SIZE_UNKNOWN = -1;
private SoundFilter soundFilter;
private int remainingSize;
/**
* Creates a new FilteredSoundStream object with the specified InputStream
* and SoundFilter.
*/
public FilteredSoundStream(InputStream in, SoundFilter soundFilter) {
super(in);
this.soundFilter = soundFilter;
remainingSize = REMAINING_SIZE_UNKNOWN;
}
/**
* Overrides the FilterInputStream method to apply this filter whenever
* bytes are read
*/
public int read(byte[] samples, int offset, int length) throws IOException {
// read and filter the sound samples in the stream
int bytesRead = super.read(samples, offset, length);
if (bytesRead > 0) {
soundFilter.filter(samples, offset, bytesRead);
return bytesRead;
}
// if there are no remaining bytes in the sound stream,
// check if the filter has any remaining bytes ("echoes").
if (remainingSize == REMAINING_SIZE_UNKNOWN) {
remainingSize = soundFilter.getRemainingSize();
// round down to nearest multiple of 4
// (typical frame size)
remainingSize = remainingSize / 4 * 4;
}
if (remainingSize > 0) {
length = Math.min(length, remainingSize);
// clear the buffer
for (int i = offset; i < offset + length; i++) {
samples[i] = 0;
}
// filter the remaining bytes
soundFilter.filter(samples, offset, length);
remainingSize -= length;
// return
return length;
} else {
// end of stream
return -1;
}
}
}