Hello! Welcome to the next lesson in your audio processing capstone project.
In our previous lessons, we focused on shaping the frequency spectrum of a signal using filters, culminating in the design of a parallel graphic equalizer. We now pivot from the frequency domain to the time domain to explore a different, yet equally powerful, class of audio effects.
Today, you will learn to implement a simple echo effect using a circular buffer for delayed signal feedback. This is a cornerstone effect, and the data structure we'll use—the circular buffer—is a fundamental tool for almost all time-based audio processing, from simple delays to complex reverbs and flangers.
1. The Anatomy of an Echo
At its core, an echo is a repetition of a sound that occurs after a delay. In the digital world, we can create this effect by storing an audio signal and playing it back later. To create the characteristic decaying echoes, we can feed a portion of the delayed signal back into the delay's input. This structure is known as a feedback comb filter.
The video below explains the core signal processing block diagram for a simple digital delay.
(Sponsored) Delay-Based Audio FX Software Implementation (DSP with STM32) - Phil's Lab #140
This video from Phil's Lab provides an excellent overview of the structure of a delay effect. It will introduce you to the key components: the delay line, the feedback loop, and the mix control.
Please watch the following two segments: The Delay Line (4:56 - 6:50): This explains the fundamental concept of a digital delay line, how delay time is measured in samples, and introduces its implementation using a circular buffer. Simple Digital Delay Structure (6:50 - 9:35): This is crucial. Pay close attention to the block diagram. Understand the roles of the input signal x[n], the feedback gain g, the delay length K, and the wet/dry mix control m.
The Difference Equation
As you saw in the video, the feedback structure can be described mathematically. Let's analyze the most common and simple form of a feedback delay, also known as a feedback comb filter.
A common structure for a simple echo effect. The input is summed with a fed-back, attenuated version of the delayed signal. This combined signal is then stored in the delay line. The final output is a mix of the original (dry) and delayed (wet) signals.
From this diagram, we can derive the difference equations:
-
The signal going into the delay line, let's call it , is the sum of the current input sample and the scaled output of the delay line from the previous step. The output of a delay line of length is the signal that was fed into it samples ago. Therefore:
where is the feedback gain. -
The final output of the effect, , is a mix of the original "dry" input signal and the "wet" signal coming out of the delay line, which is .
where is the mix level (from 0 for fully dry to 1 for fully wet).
Your electrical engineering background will recognize the first equation as a first-order infinite impulse response (IIR) filter, where the "pole" is determined by the feedback gain . For the system to be stable (i.e., for the echoes to decay and not grow infinitely loud), the magnitude of the feedback gain must be less than 1 ().
PHYSICAL AUDIO SIGNAL PROCESSING FOR VIRTUAL ...
For a more formal treatment of this structure, this classic text on Physical Audio Signal Processing by Julius O. Smith III is an excellent resource.
Please navigate to the 'Comb Filters' chapter and read the subsection on 'Feedback Comb Filters'. This section provides the formal definition, difference equation, and transfer function for the core of our echo effect.
2. The Circular Buffer: An Essential Data Structure
To implement the delay , we need to store a history of the signal . A standard audio buffer passed to our processing function is not suitable, as its contents are overwritten with each new block of audio. We need a separate, much larger buffer—the delay line—to store samples for potentially long periods.
A simple array could work, but managing it would be inefficient. We would have to shift all the samples in the array every time a new sample arrives to make room at the front. A far more efficient solution is the circular buffer (also called a ring buffer).
A circular buffer is a fixed-size array treated as if its end connects back to its beginning. We don't move any data. Instead, we use a "write pointer" (or index) that moves through the array, wrapping back to the start when it reaches the end. This elegantly solves the problem of continuously storing and retrieving the most recent samples.
JUCE Tutorial 15 - Creating a Circular Buffer
This video provides a very clear, step-by-step explanation and visualization of why a circular buffer is necessary and how it works.
Please watch the beginning of this tutorial: Introduction (0:00 - 4:07): This section clearly explains the problem: why the main audio buffer is insufficient for creating delay effects. Circular Buffer Concept (4:07 - 12:51): This is a detailed walkthrough of the circular buffer mechanism. Pay close attention to the diagrams showing the main buffer, the larger circular buffer, the moving write position, and how the data "wraps around" when it reaches the end of the buffer.
3. Implementing the Echo with a Circular Buffer
Now, let's combine the signal processing theory with the circular buffer data structure. The implementation requires managing the buffer and applying the difference equation sample by sample.
The Algorithm
For each incoming audio sample input_sample, the process is as follows:
-
Establish pointers/indices: You need a
write_indexthat points to the current position in the circular buffer. This index increments with every sample. The size of the circular buffer,buffer_size, should be large enough to accommodate the maximum desired delay. -
Calculate the read index: The "wet" signal is the sample that was written
Dsamples ago. Its location is found by subtracting the delay lengthD(in samples) from the currentwrite_index. We use modulo arithmetic to handle the "wrap-around".read_index = (write_index - D + buffer_size) % buffer_size;
(Addingbuffer_sizebefore the modulo ensures the result is always positive, which is important in some languages like C++ where%can yield negative numbers for negative inputs). -
Read the delayed sample: Get the "wet" signal from the buffer.
delayed_sample = circular_buffer[read_index]; -
Calculate the value to write: Based on our feedback equation, compute the new sample that goes into the delay line.
value_to_write = input_sample + feedback_gain * delayed_sample; -
Write to the buffer: Store this new value at the current write position.
circular_buffer[write_index] = value_to_write; -
Calculate the final output: Mix the dry input and the wet delayed signal.
output_sample = (1 - mix) * input_sample + mix * delayed_sample; -
Update the write index: Move the write index to the next position for the next sample.
write_index = (write_index + 1) % buffer_size;
The following pseudo-code from a Texas Instruments application note shows a very concise version of this processing loop.
How to Create Delay-based Audio Effects on the ...
This TI document on delay-based effects provides a clean pseudo-code implementation that is helpful for understanding the core logic.
Please examine 'Example 4. Delay Implementation'. The delay function shows the core sample-by-sample processing loop. Note how in[i] (input), w[i] (the delayed sample from the circular buffer), and out[i] (output) are combined. The line w[i] = in[i] + fb * w[i] is the feedback calculation, and out[i] = in[i] + g * w[i] is the final mix (though using slightly different variables than our model).
Test your understanding!
Imagine you have a circular buffer of size 8 (indices 0-7). The write_index is currently at 5. Your desired delay D is 3 samples. The feedback gain g is 0.5.
- Where in the buffer will you read the
delayed_samplefrom? - An
input_sampleof0.8arrives. The value at the read index is0.4. What is thevalue_to_writeback into the buffer? - Where will this new value be written?
- What will the new
write_indexbe for the next sample?
Show answer
- Read Index:
read_index = (write_index - D + buffer_size) % buffer_size = (5 - 3 + 8) % 8 = 10 % 8 = 2. You will read from index 2. - Value to Write:
value_to_write = input_sample + g * delayed_sample = 0.8 + 0.5 * 0.4 = 0.8 + 0.2 = 1.0. - Write Location: The new value will be written at the current
write_index, which is index 5. The value1.0will replace whatever was previously atcircular_buffer[5]. - New Write Index: The index is incremented and wrapped:
write_index = (5 + 1) % 8 = 6.
To see this all come together in a full C++ implementation, the following resource is excellent.
(Sponsored) Delay-Based Audio FX Software Implementation (DSP with STM32) - Phil's Lab #140
Finally, let's see how the theory translates into a concrete C implementation. Phil's Lab demonstrates building this exact effect on a microcontroller.
Please watch the 'Code Implementation' section from 13:49 to 20:49. Observe how a struct is used to hold the delay line parameters and the buffer itself. Pay close attention to the update function, as it directly implements the algorithm we just discussed: reading from the buffer, computing the feedback, writing back to the buffer, and managing the index.
Conclusion
Congratulations! You've now grasped the theory and implementation of one of the most fundamental audio effects. The concepts you've learned today are the bedrock for a wide array of more complex time-based effects.
Key Takeaways:
- A simple echo effect is created using a feedback delay structure, which is a type of IIR comb filter.
- The key parameters are delay time (in samples), feedback gain (controls decay), and wet/dry mix.
- A circular buffer is the efficient data structure used to implement the digital delay line, avoiding costly data shifting by using a wrapping write pointer.
- The core algorithm involves reading a past sample from the buffer, calculating a new sample using the feedback equation, writing the new sample back to the buffer, and advancing the write pointer.
Preview of the Next Lesson:
What happens if the delay time D is not fixed? In the next lesson, we will explore this by learning to develop a chorus or flanger effect by modulating the delay time of a delay line with a low-frequency oscillator (LFO). This will build directly upon the circular buffer and delay line structure you've learned to implement today.