Creating a Real-Time Echo Filter

You've just bought an SUV, and you decide to do what they do in those SUV commercials on TV: drive to the top of a mountain. Everyone who owns an SUV does that, right? When you get to the top of the mountain, you step outside of your vehicle, absorb the beautiful scenery, and shout, "Hello!" A second later, your echo replies in a quieter voice: "Hello!" Another second later, you might hear a second reply, even quieter than the first. You've just discovered the two elements of an echo: delay and decay, as shown in . The delay is how long it takes for the echo to occur. The decay is how much quieter the echo was compared to the original sound. In the case of you shouting on top of the mountain, the delay was about one second, and the decay was probably less than 50%. A decay value of 100% means the echo never dies out.

Screenshot An echo repeats the original sound after a delay. The echoes are quieter than the original sound, at about a 60% decay.

Java graphics 04fig02

Because your SoundFilter class is format-agnostic, it doesn't matter whether the sound you are filtering has a 44,100Hz sample rate or an 8,000Hz sample rate. So, you can't simply tell the echo filter the amount of time to delay. Instead, you tell it how many samples to delay. For a 44,100Hz sample rate and a one-second delay, tell the filter to delay by 44,100 samples. Remember, the delay counts from the beginning of the sound, not the end. So, if you want to delay one second after a 44,100Hz sound is finished, set the delay to the number of samples in the sound plus 44,100. Usually, though, you'll want the same echo delay across all sounds, which means that with longer sounds, the first echo might kick into action while the original sound is still playing. The decay itself is simple enough. Use a value from 0 to 1, where a decay of 0 means no echo and a decay of 1 means the echo is the same volume as the original sound. Be careful with values close to 1, though. The "Hello!" example is innocent enough because the echo takes place after the sound is finished, but for many sounds, the echo occurs while the sound is still playing. For decay values close to 1, this will mostly likely lead to distortion because the parts of the original sound added to the echo could be larger that the 16-bit range of the sample. When you're done exploring the mountain with your SUV, let's create the EchoFilter class. The EchoFilter class, in , is a subclass of SoundFilter. To implement the echo, it keeps a delay buffer, which is a buffer of the same length as the amount of samples to delay. Whenever a sample is processed, the sample is copied to the delay buffer. This makes creating an echo effect easy. The echo is created by adding a decayed sample from the delay buffer to the original sample:

short newSample = (short)(oldSample + decay *
 delayBuffer[delayBufferPos]);

Because the delay buffer starts off as all zeroes, or silent, the first bit of sound heard is the original sound with no filter. Also, after the original sound is done playing, the FilteredSoundStream makes sure to keep filtering, using a silent original sound. That way, just the echoes are still heard. That's right, everything is happening according to plan. Finally, getRemainingSize() is implemented to calculate the number of bytes that could be filtered after the original sound is done. Here, you use a calculation to make sure the final echo has 1% of the original sound's volume, which is close enough to being silent.

Listing 4.5 EchoFilter.java

package com.brackeen.javagamebook.sound;
/**
 The EchoFilter class is a SoundFilter that emulates an echo.
 @see FilteredSoundStream
*/
public 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
 multiplied 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;
 }
 }
 }
}

As mentioned before, a new SoundFilter should be created for every sound played. Several sounds using the same EchoFilter could cause some weird things to happen because each sound would be copied to its delay buffer. If you need to reuse SoundFilters after they are done, call the reset() method. For an example of how to use the EchoFilter, see EchoFilterTest in . This example adds only a couple steps to SimpleSoundPlayer's example: namely, creating an EchoFilter and a FilteredSoundStream.

Listing 4.6 EchoFilterTest.java

import java.io.*;
import com.brackeen.javagamebook.sound.*;
/**
 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);
 }
}

This example uses a 1/4th second delay and a 60% decay to give the playing voice a space-age echo. The echo filter is finished and working great, so now you'll create another filter-one to emulate 3D sound.