in development

let’s make lots of echoes: building a simple reverb

Having drifted into video for a bit, I’m back into audio, and putting together an exciting thing that I’ll be releasing soon. For this new project, I needed to sweeten the output of an audio synthesizer: add a little warmth, a little depth, that sort of thing. Give it atmosphere. How about some reverb? Sounds good!

So, what exactly is reverb? It’s the sum total of many, many echoes combining with the original signal. Imagine a musician playing in a large chamber. Sound waves from her instrument will bounce from the near wall, the far wall, the ceiling above her head, the floor twenty feet away, and so on, and each echo will bounce into other bits of the chamber, and so on, and so on. The echo intervals are very small–like fractions of a millisecond small. The totality of all these echoes, and echoes of echoes, produces a sort of low mutter that decays slowly. When applied to an audio signal it adds a sense of space, of location.

There are many algorithms that can provide reverb. Some involve convolution, others combine filters. I didn’t need anything too complex or configurable, and it’s been said that one can build nearly any effect out of delay lines. I decided to give it a try. Here’s what I came up with.

/**
 * implement circular delay buffer with multiple feedback paths
 */
public class Delay {

	static int Length = 8192;
	static int Modulus = Length - 1;

	static float Mixer = 0.95f;

	float[] buffer = new float[Length];
	int reader;
	int writer = Length >> 1;
	int[] path = new int[8];

	public Delay() {
		relocate();
	}

	/**
	 * recreate feedback paths to relocate echoes
	 */
	public void relocate() {
		Random rng = new Random();
		for (int i = 0; i < path.length; i++) {
			path[i] = rng.nextInt();
		}
	}

	/**
	 * process the next sample
	 */
	public float process(float s) {
		for (int i = 0; i < path.length; i++) {
			int w = (writer + path[i]) & Modulus;
			buffer[w] = s * (1f - Mixer) + buffer[w] * Mixer;
		}
		writer++;
		return buffer[(reader++) & Modulus] + s;
	}

}

Let’s see. The buffer size isn’t configurable but could be made so. Just make sure it’s a power of two, as I’m using one of my favorite buffer tricks: bitwise-AND modulus, which prevents indices from running over the length without hitting a conditional or an expensive divide-based modulus.

The path array represents a small number of feedback paths randomly distributed across the buffer that provide echo sources. Calling relocate() will shuffle them around. The user calls process() and supplies an audio sample, receiving in return the sample mixed with reverb. Inside the call, the sample is placed wherever the paths dictate, blending it with whatever’s already there.

The effect it produces is not nearly as impressive as you can get with more complex algorithms or DSP hardware, but for my application, it’s perfect. It gives the flat output of the synthesizer a subtle depth–a sense of having been recorded live rather than generated, and that’s all I needed.

ADDENDUM:

For best performance, you’ll want to submit a sample buffer to the delay object instead of trying to process one sample at a time. Here’s an implementation of the process() method that does this.

public void process(float[] sample) {
	for (int i = 0; i