Everyday DSP for Programmers: Signal Envelopes

Sometimes we don't care so much about the exact details of a signal as we do about whether a signal is even present or not. If the signal is periodic, it can be difficult to directly detect when the signal is there and when it goes away because when it is present, it's oscillating between various values. In cases like these, what we want to calculate is the envelope of the signal, which gives us the information about whether the signal is there or not and what its approximate amplitude is. As an added bonus, calculating a signal envelope can be done in real-time, so other DSP functions can be done on the signal as soon as it's detected.

The Signal

For this exercise we'll use the following signal as an example (click to start the signal in motion):

The signal starts with zero amplitude and a small amount of noise. Then it will randomly change amplitude over time, and we want to get an estimate of what that amplitude is at any given point so we can do further processing when the signal is present. Common applications where this type of signal detection would come up are when working with audio signals or carrier waves for radio signals.

Removing the DC Component

The first thing to notice about the signal above is that it has a DC component to it. That DC component is a common characteristic in periodic signals, and it's going to make further processing more difficult, so we want to remove it. The most straightforward way to do that is to use a running average to calculate what the DC component is, and then subtract that value from every incoming sample. It may seem like we use averaging a lot in DSP, and I'm not going to deny it. Averaging is the hammer of the DSP toolbox, and an awful lot of DSP problems look like nails. We're going to hammer away.

Like everything we've done so far, the code for this operation is simple. We'll continue using the exponential average for our averaging method. Here it is in JavaScript:

function RemoveDC(sample, w) {
avg = w*sample + (1-w)*avg
return sample - avg

The value of w needs to be adjusted, depending on the expected signal characteristics, so that the exponential average is slow enough to smooth out all transient components of the signal, leaving only the DC component. It's not shown here, but avg should be part of a signal object and set to the first incoming sample. RemoveDC() would be a method and a closure of that signal object. Other types of architectures are also possible, depending on how the samples are coming into the system and if calculations need to be done in real-time or not. The resulting signal with the DC  component removed will look like this:

It's purely a shift of the signal so that it's centered around the x-axis. The resulting signal is mostly off the edge of this graph, but for future graphs, we'll start with the DC component removed from the signal and the x-axis in the center of the graph.

Move it All to One Side

Now we're ready to start calculating the envelope. Having the signal on both sides of the x-axis complicates things, and if you think about it for a while, you should convince yourself that any calculation that deals with positive and negative values is going to be convoluted at best. Let's sidestep that problem all together by taking the absolute value of every sample. It's an easy calculation, and it dramatically simplifies things. Here's the code:
function CalculateEnvelope(sample, w) {
return Math.abs(RemoveDC(sample, w))

And here's what the absolute value operation does to the signal:

Now all of the signal values are positive, and the signal envelope is getting more well-defined. Can you guess what we're going to do next?

Remove the Ripple

To remove that large, spiky ripple from the signal, we're going to (surprise, surprise) do another exponential average. This time, instead of giving us the DC component of the signal, the exponential average will smooth out the ripples and give us the signal envelope. The weighting for the average should be larger than the one used for the DC component because we want it to react faster when the signal increases and decreases in amplitude. The code for this operation is pretty simple again:
function CalculateEnvelope(sample, w_dc, w_env) {
pos_sample = Math.abs(RemoveDC(sample, w_dc))
avg_env = w_env*pos_sample + (1-w_env)*avg_env
return avg_env

Like the average in RemoveDC(), the avg_env term should be a member of the enclosing signal object so that it gets updated for each additional sample. The weightings need to be differentiated now since there are two of them. The w_env weighting needs to be selected so that the average removes enough of the ripple that the envelope can be compared to a threshold, but the average still reacts quickly enough to the signal for any delay requirements that you might have. The envelope isn't going to be perfect, but the higher the signal frequency is relative to how quickly the amplitude changes, the easier it is to choose a good weighting.

Here is what the envelope looks like for our signal:

The white signal is the envelope, and it does a pretty good job of showing where the signal amplitude is at any given time. It doesn't reach the full amplitude of the original signal, but they're highly correlated so this estimated envelope can still be used in many applications. Taking the absolute value of the signal so that both the positive and the negative peaks are contributing to the exponential average doubles the effectiveness of the envelope calculation because it has twice as many peaks to push up the average. If the negative envelope is also needed, it's easy enough to mirror the envelope across the x-axis by taking the negative of each sample. The combination of the positive and negative envelopes gives us the bounded envelope of the signal. Now that we have the envelope, there are a couple other things we can do with it.

Amplitude and Duration

The envelope is an indirect measure of the signal's amplitude, and by setting a threshold, we can use the envelope as a gating signal for other processing. When the envelope crosses above the threshold, we can do any other necessary processing on the signal; and when it crosses below the threshold, the signal is gone and we can hold processing and do other necessary tasks, like clean up or setup for when the signal comes back.

One type of processing that we can do is simply count how long the envelope is above the threshold. The duration of the signal could be an important parameter, and now it's pretty easy to calculate. Here is the code:

function CalculateDuration(sample, w_dc, w_env, threshold, duration) {
envelope = CalculateEnvelope(sample, w_dc, w_env)
if (envelope > threshold) {
duration += 1
} else {
duration = 0
return duration

This function will create a signal that counts up as long as the envelope is above a threshold. Whenever it crosses below the threshold, the returned value resets to zero. Here's what that signal looks like visually:

The purple line is the threshold and the green signal is the duration count, and that could also be compared to a threshold to mark the signal as valid or trigger other processing. Also notice that the signal envelope looks quite a bit like the signal we were doing edge detection on in the last post, and in fact, it is possible to chain edge detection with envelope calculation to detect when the amplitude of a signal is changing. Combining different DSP algorithms yields new designs for solving different types of problems.

All in all, this was another simple set of operations to calculate a useful parameter of a signal. Much of signal processing is like this, where the creative combination of simple tools results in efficient, useful metrics from signals. Calculations can certainly get more complex than this, but the basic ideas are usually straightforward. The complexity comes from trying to dial in the parameters of an algorithm or handling pathologic edge cases in the signal that seem to require adding more and more operations until the algorithm looks like a house of cards.

But I digress. Next week we'll look at another simple algorithm that can be used as a building block: frequency measurement.

No comments :