Development Class Java

/*
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:
 * 

     * 
  • Playing a Midi sequence.
     * 
  • Toggle a track of a playing Midi sequence.
     * 
  • Playing a sound.
     * 
  • Playing a Sound with an Echo filter.
     * 
  • Looping a sound.
     * 
  • Playing the maximum number of sounds at once.
     * 
  • Pausing all sounds.
     * 

 * 


 * 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:
 * 


     * 
  • add a setMasterVolume() method, which uses Controls to set the volume
     * for each line.
     * 
  • don't play a sound if more than, say, 500ms has passed since the request
     * to play
     * 

 */
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.
   * 


   * 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:
 * 


     * 
  • Add an option to only loop a certain number of times.
     * 

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