EchoFilterTest.java Source code

Java tutorial

Introduction

Here is the source code for EchoFilterTest.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.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.UnsupportedAudioFileException;

/**
 * An example of playing a sound with an echo filter.
 * 
 * @see EchoFilter
 * @see SimpleSoundPlayer
 */
public class EchoFilterTest {

    public static void main(String[] args) {

        // load the sound
        SimpleSoundPlayer sound = new SimpleSoundPlayer("../sounds/voice.wav");

        // create the sound stream
        InputStream is = new ByteArrayInputStream(sound.getSamples());

        // create an echo with a 11025-sample buffer
        // (1/4 sec for 44100Hz sound) and a 60% decay
        EchoFilter filter = new EchoFilter(11025, .6f);

        // create the filtered sound stream
        is = new FilteredSoundStream(is, filter);

        // play the sound
        sound.play(is);

        // due to bug in Java Sound, explicitly exit the VM.
        System.exit(0);
    }

}

/**
 * The SimpleSoundPlayer encapsulates a sound that can be opened from the file
 * system and later played.
 */

class SimpleSoundPlayer {

    public static void main(String[] args) {
        // load a sound
        SimpleSoundPlayer sound = new SimpleSoundPlayer("../sounds/voice.wav");

        // create the stream to play
        InputStream stream = new ByteArrayInputStream(sound.getSamples());

        // play the sound
        sound.play(stream);

        // exit
        System.exit(0);
    }

    private AudioFormat format;

    private byte[] samples;

    /**
     * Opens a sound from a file.
     */
    public SimpleSoundPlayer(String filename) {
        try {
            // open the audio input stream
            AudioInputStream stream = AudioSystem.getAudioInputStream(new File(filename));

            format = stream.getFormat();

            // get the audio samples
            samples = getSamples(stream);
        } catch (UnsupportedAudioFileException ex) {
            ex.printStackTrace();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Gets the samples of this sound as a byte array.
     */
    public byte[] getSamples() {
        return samples;
    }

    /**
     * Gets the samples from an AudioInputStream as an array of bytes.
     */
    private byte[] getSamples(AudioInputStream audioStream) {
        // get the number of bytes to read
        int length = (int) (audioStream.getFrameLength() * format.getFrameSize());

        // read the entire stream
        byte[] samples = new byte[length];
        DataInputStream is = new DataInputStream(audioStream);
        try {
            is.readFully(samples);
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        // return the samples
        return samples;
    }

    /**
     * Plays a stream. This method blocks (doesn't return) until the sound is
     * finished playing.
     */
    public void play(InputStream source) {

        // use a short, 100ms (1/10th sec) buffer for real-time
        // change to the sound stream
        int bufferSize = format.getFrameSize() * Math.round(format.getSampleRate() / 10);
        byte[] buffer = new byte[bufferSize];

        // create a line to play to
        SourceDataLine line;
        try {
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(format, bufferSize);
        } catch (LineUnavailableException ex) {
            ex.printStackTrace();
            return;
        }

        // start the line
        line.start();

        // copy data to the line
        try {
            int numBytesRead = 0;
            while (numBytesRead != -1) {
                numBytesRead = source.read(buffer, 0, buffer.length);
                if (numBytesRead != -1) {
                    line.write(buffer, 0, numBytesRead);
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }

        // wait until all data is played, then close the line
        line.drain();
        line.close();

    }

}

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

}

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

}

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

}