SoundManagerTest.java Source code

Java tutorial

Introduction

Here is the source code for SoundManagerTest.java

Source

/*
DEVELOPING GAME IN JAVA 
    
Caracteristiques
    
Editeur : NEW RIDERS 
Auteur : BRACKEEN 
Parution : 09 2003 
Pages : 972 
Isbn : 1-59273-005-1 
Reliure : Paperback 
Disponibilite : Disponible a la librairie 
*/

import java.awt.Color;
import java.awt.Container;
import java.awt.DisplayMode;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferStrategy;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.LinkedList;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
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 javax.swing.AbstractButton;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JToggleButton;
import javax.swing.RepaintManager;

/**
 * The SoundManagerTest demonstrates the functionality of the SoundManager
 * class. It provides the following demos:
 * <ul>
 * <li>Playing a Midi sequence.
 * <li>Toggle a track of a playing Midi sequence.
 * <li>Playing a sound.
 * <li>Playing a Sound with an Echo filter.
 * <li>Looping a sound.
 * <li>Playing the maximum number of sounds at once.
 * <li>Pausing all sounds.
 * </ul>
 * <p>
 * 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.
 * <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>
 */

class SoundManager extends ThreadPool {

    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(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);
        return mixer.getMaxLines(lineInfo);
    }

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

        }
    }

}

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.
     * <p>
     * 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.
     * <p>
     * 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.
     * <p>
     * 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.
     * <p>
     * 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.
 * <p>
 * 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.
     * <p>
     * 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.
 * <p>
 * Possible ideas to extend this class:
 * <ul>
 * <li>Add an option to only loop a certain number of times.
 * </ul>
 */

class LoopingByteInputStream extends ByteArrayInputStream {

    private boolean closed;

    /**
     * Creates a new LoopingByteInputStream with the specified byte array. The
     * array is not copied.
     */
    public LoopingByteInputStream(byte[] buffer) {
        super(buffer);
        closed = false;
    }

    /**
     * Reads <code>length</code> 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;
        }
    }

}