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