JoshJers' Ramblings

Infrequently-updated blog about software development, game development, and music

Posts tagged “fft”

FFTs Part 1: The Discrete Fourier Transform

Because the world clearly doesn’t already have enough of them, I recently released a new FFT library: FFT-20XX. To the best of my ability to tell, it uses an algorithm that is at least slightly novel, and I thought it might be interesting to write about it.

This is the first of a series of posts about FFTs and (eventually) the specific algorithms that FFT-20XX uses:

  1. The Discrete Fourier Transform (← you are here)
  2. The Basics of the FFT
  3. Some Optimizations to the FFT
  4. The Inverse FFT
  5. The FFT-20XX Complex-Input Algorithm
  6. The FFT-20XX Real-Input Algorithm(s)

As a first step I thought it might be fun to start with “what even is the discrete Fourier transform?”

Fourier Transforms

Any explanation of a Fourier transform would be remiss to not include a link to this fabulous 3Blue1Brown video that nicely illustrates how the Fourier transform works. I highly recommend watching it!

This post, however, is specifically about the discrete Fourier transform (referred to as the DFT), which is a Fourier transform that operates on sampled data (like, say, digital audio or images) instead of continuous data. The FFT (fast Fourier transform) is the standard algorithm used to compute the result of a DFT — we’ll get to it in the next post.

What Does the DFT Even Do?

Any periodic signal (i.e. a signal that repeats, such as looped audio) can be represented as the sum of a set of sine waves at different frequencies. Here is an example of a simple signal that can be broken down into three waves:

Diagram of a somewhat complex wave, which is composed of the three following waves
=
Diagram of the lowest frequency and highest-amplitude wave that makes up the above complex wave.
+
Diagram of the middle frequency wave that makes up the above complex wave. It has an amplitude lower than the first component wave.
+
Diagram of the highest-frequency wave that makes up the above complex wave. It has a lower amplitude than the first two.

This is true of any periodic signal whether it is analog or sampled — no matter how complex it may seem.

The DFT answers a simple question: given a set of samples that represents a periodic signal, which frequencies are represented in that signal?

Note

The “periodic signal” bit is important! The algorithm assumes that the block of samples it is processing repeats forever:

Diagram showing a signal that repeating three times.

However, this won’t be the case for all blocks, such as this one where there’s a hard discontinuity:

Diagram showing a signal that repeats three times, however this time the wave in each signal do not line up properly at the edges, causing a discontinuity.

This can cause noise in the resulting DFT output. There are a few tricks that can be done for cases like this (like windowing the signal) to effectively “pretend” that it is periodic when it’s not, but that’s out of scope for this post.

To put it another way: the DFT converts a signal that is made up of samples in time (where each sample represents a point on the signal at a given time) into the same signal represented in frequency (where each sample represents the amplitude (how tall the wave is) and phase (the side-to-side position of the wave) of its corresponding frequency). These two representations are frequently referred to as time domain and frequency domain, and both encode the same signal in different ways.

Note

There is also an inverse DFT (the IDFT) which converts the other way — from the frequency domain back to the time domain. Many signal processing applications convert from time to frequency space to do some operation, then use the inverse DFT to convert back. We’ll get to the inverse DFT in a later post.

For a given repeating signal that is N samples long, the DFT returns contribution values for N frequencies, each with one more cycle than the previous. To put it a different way: for each frequency with index k (starting at 0 and ending at N - 1) there are k cycles of a wave (so index 0 has 0 cycles, index 1 has 1 cycle, etc).

Here are the 8 waves for a DFT with 8 samples (the red wave with circles is the cosine and the green wave with squares is the sine):

0
Diagram of Frequency 0: all of the cosine values are '1' and all of the sine values are '0'. There are 8 points on each wave representing the samples (this is true for all subsequent waves as well).
1
Diagram of Frequency 1: there is a single up to down to up cycle of the cosine and sine waves.
2
Diagram of Frequency 2: there are two up to down to up cycles of the cosine and sine waves.
3
Diagram of Frequency 3: there are three up to down to up cycles of the cosine and sine waves.
4
Diagram of Frequency 4: there are four up to down to up cycles of the cosine and sine waves. Notably, the cosine samples alternate between '1' and '-1', and the sine samples are all '0'.
5
Diagram of Frequency 5: there are five up to down to up cycles of the cosine and sine waves.
6
Diagram of Frequency 6: there are six up to down to up cycles of the cosine and sine waves.
7
Diagram of Frequency 7: there are seven up to down to up cycles of the cosine and sine waves.

Ultimately, a DFT will give you a set of values that tell you how much each of these frequencies contributes to the signal. Specifically, each frequency will have a value that represents both its amplitude and its phase.

Note

The frequency at index 0 always has cosine values of 1 and sine values of 0, and this ends up measuring how “off-center” the signal is (relative to the x axis). This is known as the DC bias or DC offset.

A signal with no DC bias. The wave is centered on the x axis
No DC bias
A signal with positive DC bias. The wave is above the x axis
Positive DC bias
A signal with negative DC bias. The wave is below the x axis
Negative DC bias

Let’s figure out how to calculate the DFT for a signal!

Frequency Testing

The first step of computing the DFT is to figure out how to get a single frequency out of a signal. Ignoring phase for a moment, this can be done in a surprisingly straightforward manner (to me, at least): if we multiply each sample in the signal by the cosine value of the test frequency and sum the results, you end up with a value that represents the contribution of that frequency to the signal (which is 0 if the frequency is not present in the signal).

Here is an interactive example where you can set the signal and test frequencies and see the result of this process. For purposes of illustration, we’ll display the average instead of the final sum (as the sum would be too large to fit in the diagram):

An interactive diagram that demonstrates how the signal and test waves are processed to detect frequencies. × Interactive diagram = avg →
Signal Amplitude:
1.0
Signal Cycles:
2
Test Cycles:
2

Note that when the frequencies match (signal and test cycles), the resulting average scales with the amplitude of the signal, otherwise (with one exception we’ll touch on in a moment) it is 0. This makes this sum effectively a frequency detector (again, ignoring phase for now)! This is still true even when there are multiple frequencies represented within the signal: this will result in a zero value for frequenices that are not in the signal and non-zero values for those that are.

Math Time!

If you’re interested in the math of how this works, here you go:

The sum of all samples of a repeating cos⁡ x\cos\,x signal is 00, as it has the same area above and below the xx axis, which cancels out in the sum.

Now, if we multiply two such waves together: cos⁡ x⋅cos⁡ y\cos\,x \cdot \cos\,y, this can be rewritten (via trig identities) as:
 12[cos⁡(x−y)+cos⁡(x+y)]\frac{1}{2}[\cos (x - y) + \cos(x + y)]

If xx and yy don’t match, these are just two repeating cosines, and each of them sums to 00 over all of the samples (just like the single wave above).

However if x=yx = y, this becomes:
 12[cos⁡(x−x)+cos⁡(x+x)]  ⟹  12[cos⁡ 0+cos⁡ 2x]\frac{1}{2}[\cos (x - x) + \cos(x + x)] \implies \frac{1}{2}[\cos\,0 + \cos\,2x]
   ⟹  12+12cos⁡ 2x\implies \frac{1}{2} + \frac{1}{2}\cos\,2x

That remaining cos⁡\cos term, over all of the samples, once again sums to 0, which leaves us with a constant 12\frac{1}{2} in the average.

As noted, there’s an exception here, where the multiply-and-sum seems to register a match when it shouldn’t. For example: if you set the Signal Cycles to 1 and the Test Cycles to 15 (or vice versa), it will have a non-zero average even though the frequencies don’t match. In fact, with the exception of frequencies 0 and 8, there are two matches for every other frequency. Why is that?

Well, now it’s time to talk about…

Aliasing and Negative Frequencies

Let’s take a closer look at the cosine values for our example frequencies of 1 and 15 cycles:

1
Diagram of the cosine for frequency 1 with sixteen samples. The wave has a single cycle.
15
Diagram of the cosine for frequency 15 with sixteen samples. The wave has fifteen cycles. The sample positions in this diagram are exactly the same as in the diagram of frequency 1.

Note that while the waves are different frequencies, the cosine samples are the same! This is because of aliasing, which is where a frequency above the maximum representable frequency ends up sampling as if it were a different, lower frequency.

The frequency above which aliasing occurs is called the Nyquist frequency, which is the highest frequency where there can be both a high and low sample for every cycle (you need both to represent a wave). In a signal with N samples, that frequency has N / 2 cycles. For example, with 16 samples the Nyquist frequency has 8 cycles (which you can see have a clear up then down pattern):

Diagram of the cosine for frequency 8 with sixteen samples. The wave has eight cycles. Every sample on the cosine wave alternates between '1' and '-1'

Above this frequency, the cosine samples end up aliasing to a lower frequency and cannot be differentiated anymore. Here are some more example pairs of aliased cosines from our 16-sample example:

2
Diagram of the cosine for frequency 2 with sixteen samples. The wave has two cycles.
14
Diagram of the cosine for frequency 14 with sixteen samples. The wave has fourteen cycles. The sample positions in this diagram are exactly the same as in the diagram of frequency 2.
3
Diagram of the cosine for frequency 3 with sixteen samples. The wave has three cycles.
13
Diagram of the cosine for frequency 13 with sixteen samples. The wave has thirteen cycles. The sample positions in this diagram are exactly the same as in the diagram of frequency 3.
7
Diagram of the cosine for frequency 7 with sixteen samples. The wave has seven cycles.
9
Diagram of the cosine for frequency 9 with sixteen samples. The wave has nine cycles. The sample positions in this diagram are exactly the same as in the diagram of frequency 7.

Note that each pair has matching cosine samples, despite the different wave. Specifically: with a repeating sample count of N, any frequency with N−kN - k cycles has the same cosine values as the frequency with kk cycles (that is 1 matches 15, 2 matches 14, etc).

Note

This is just like the optical illusion where rapidly-spinning things like car wheels can appear to be rotating backwards.

That’s neat! But it’s not the whole picture. We can’t just look at the cosine values, we need to look at what happens to the sine values as well:

1
Diagram of the sine for frequency 1 with sixteen samples. The wave has a single cycle.
15
Diagram of the sine for frequency 15 with sixteen samples. The wave has fifteen cycles. The sample positions in this diagram are like the ones in the diagram of frequency 1, but negated.
2
Diagram of the sine for frequency 2 with sixteen samples. The wave has two cycles.
14
Diagram of the sine for frequency 14 with sixteen samples. The wave has fourteen cycles. The sample positions in this diagram are like the ones in the diagram of frequency 2, but negated.
3
Diagram of the sine for frequency 3 with sixteen samples. The wave has three cycles.
13
Diagram of the sine for frequency 13 with sixteen samples. The wave has thirteen cycles. The sample positions in this diagram are like the ones in the diagram of frequency 3, but negated.
7
Diagram of the sine for frequency 7 with sixteen samples. The wave has seven cycles.
9
Diagram of the sine for frequency 9 with sixteen samples. The wave has nine cycles. The sample positions in this diagram are like the ones in the diagram of frequency 9, but negated.

While the cosines are the same in these pairs of frequencies, the sines are negated and appear vertically flipped! This means that a given frequency with N−kN - k cycles has negated sine values from the frequency with kk cycles.

The relationship here is that the aliased frequencies in each pair are negated versions of the other, since cos⁡(−a)=cos⁡(a)\cos({-}a) = \cos(a) and sin⁡(−a)=−sin⁡(a)\sin({-}a) = {-}\sin(a). Thus, frequency N−kN - k has the same angle as the hypothetical frequency at −k{-}k:

Cosines:

-1
Diagram of the cosine for a hypothetical frequency -1 with sixteen samples. The wave has a single cycle.
15
Diagram of the cosine of frequency 15 with sixteen samples. The wave has fifteen cycles. The sample positions in this diagram match the ones in the cosine diagram for hypothetical frequency -1 exactly.

Sines:

-1
Diagram of the sine for a hypothetical frequency -1 with sixteen samples. The wave has a single cycle, negated vs a standard sine wave.
15
Diagram of the sine of frequency 15 with sixteen samples. The wave has fifteen cycles. The sample positions in this diagram match the ones in the sine diagram for hypothetical frequency -1 exactly.

In practice, when computing the DFT, the frequencies above Nyquist are referred to via their negative indices instead of as the positive aliased ones.

This explains why all of the frequencies in the above example (except DC and Nyquist) have two matches: the cosines match at both the positive and negative versions of the angle. This may seem redundant, but it isn’t always — we’ll get into why in a bit.

Note

It’s also worth noting that the multiply-then-average in the example diagram results in half the signal amplitude for each match, and when both are summed, the result represents the full amplitude!

However, DC and Nyquist — because they each only have a single match — have an average that matches the amplitude exactly.

Measuring Phase

So far we have been looking at a signal that contains a cosine, comparing it with a test wave that is also a cosine, and getting an amplitude-relative result. But the original description of the DFT mentions both amplitude and phase as being represented, so what happens if we let the phase of the signal shift around relative to the test?

An interactive diagram that demonstrates how adjusting the the signal phase affects the average. × = Interactive diagram avg →
Signal Phase:
0.0

Oh no! Sliding the phase around causes the resulting average to oscillate! While we get the expected 0.5 average when the phase is 0, it is -0.5 at a phase of 0.5 (a half-wavelength offset) and, even worse, 0 at phases of 0.25 and 0.75 (quarter- and three-quarter-wavelength offsets)! Clearly there is something else we need to consider here…and it turns out it’s the sine of the test frequency.

Instead of just multiplying by the cosine of the test value, we can instead multiply by both cosine and sine to get two values per sample. If we then treat those two resulting values as a 2D coordinate (where cosine corresponds to x and sine to y), we can average them all to get our final result, which will be at the origin for non-matching frequencies and at some non-origin position for matches.

Note

“Multiply by both sine and cosine” is a simplification for purposes of illustration. It works for now but is not the general form, which we’ll get to in a bit.

An interactive diagram that demonstrates how adjusting the the signal and test waves affects a 2D average. × Interactive diagram = avg → Amp: 0.5 Phase: 0.00
Signal Phase:
Signal Cycles:
Test Cycles:

Some observations from this diagram:

  • If the cycle value magnitudes don’t match, the points are all evenly distributed around the center (and thus the average is at the origin). This makes it much easier to visualize why we end up with no measured amplitude for mismatched waves.

  • If the cycle value magnitudes do match (i.e. a signal of 2 cycles with either 2 or -2 test cycles), the points are off-center, and thus their average is also off-center.

  • Increasing the phase causes the resulting average to spin clockwise when the test cycle count is positive, but counter-clockwise when the test cycle count is negative. The magnitude of the average remains the same no matter the phase.

  • At Nyquist (8 cycles), the resulting points always lie on the x axis, as the test wave’s sine values are all zeros. It turns out: a phase-shifted wave at Nyquist is indistinguishable from a non-shifted wave with scaled amplitude (because there is literally no way to represent such a phase shift with the given sample rate).

This gives us a new way to think about this computation: rather than thinking of the operation as a multiplication, instead we can think of each sample of the signal being rotated by the angle of the test wave at that sample position to get a new 2D position. Those 2D positions are then summed to get a value that corresponds to the frequency’s contribution.

The computed amplitude (the strength of this frequency’s contribution) is the magnitude of the resulting average (its length), and the phase is the angle of the average (computed, in this case, using atan2).

2D Coordinates and Complex Numbers

Each result of this multiplication with cosine and sine ends up as a 2D coordinate. Canonically, the Fourier transform represents these as a complex number (of the form x+i⋅yx + i \cdot y). Why complex numbers? Honestly I think it’s a hack to get a 2D coordinate that is simple to represent with standard math notation (mathemeticians, don’t @ me).

Note

If you, like me, prefer 2D vectors to complex numbers, just pretend that x+iyx + iy is the 2D vector [ x   y ][\,x\,\,\,y\,] as you read all of this: the real component is the xx coordinate and the imaginary component is the yy coordinate.

To be fair, complex numbers do have a nice property that standard 2D vectors do not: they can be multiplied in a meaningful way.

The specific complex multiplications done by the DFT (an input sample [a+iba + ib] multiplied by a wave value [cos t+i sin tcos\,t + i\,sin\,t]) ends up rotating the input by the angle tt!

Note

If you’re familiar with matrices and linear algebra, this complex multiply (with cc and ss representing the cosine and sine values):

(x+iy)⋅(c+is)(x + iy) \cdot (c + is)

  ⟹  (xc−ys)⋅i(xs+yc)\implies (xc - ys) \cdot i(xs + yc)

…results in exactly the same operations as multiplying a 2D vector (x, y) with a 2x2 rotation matrix representing the same angle:

[xy]⋅[cs−sc]\begin{bmatrix}x & y\end{bmatrix} \cdot \begin{bmatrix} c & s \\ -s & c \end{bmatrix}

  ⟹  [xc−ysxs+yc]\implies\begin{bmatrix} xc - ys && xs + yc \end{bmatrix}

This also finally gives us an answer to the earlier question of why we need both the positive and negative versions of a given frequency: if the input samples are complex (i.e. 2D) instead of purely real-valued (1D), the contributions to these paired frequencies can be different, and both are needed to fully reconstruct the signal.

For real-valued signals, however, the paired results will be complex conjugates, where the result (a+iba + ib) for frequency kk is the conjugate of the result for frequency −k{-}k: (a−iba - ib); they have the same real (x) term but opposite imaginary (y) terms.

When summed, these values (a+iba + ib and a−iba - ib) become 2a2a as the imaginary components cancel out, which is how a signal made up of real cos⁡\cos terms and imaginary sin⁡\sin terms can be a purely real-valued signal.

Note

It’s worth pointing out something that shows up in the standard formula for the DFT: mathematical notation has an absolutely wild shorthand for cos⁡ t+i sin⁡ t\cos\,t + i\,\sin\,t:

eite^{it}

That’s right, taking the mathematical constant ee to an imaginary power is the same as a rotation around the origin. Why? Well, 3blue1brown has an explanation if you’re curious.

Personally I don’t like this shorthand because it obscures the meaning of the equation, especially for non-mathemeticians (which is, if I’m doing my calculations right, most people).

Calculating the DFT

Okay, we finally have all of the pieces of how to calculate a DFT, it’s time to put them together.

A DFT takes a set of complex input samples in the time domain and produces the same number of complex output samples in the frequency domain, where the first output sample (at index 0) represents the DC offset, the middle one (at index N/2) represents the Nyquist frequency, and the rest are either the positive or negative angles in between.

There are a couple minor (but important) differences in the value calculation done in the above interactive diagrams vs how the DFT does them:

  • Typically the DFT rotates the opposite direction as the above diagrams (i.e. using sin⁡(−t)\sin({-}t) instead of sin⁡ t\sin\,t), so the rotations actually spin the opposite way (clockwise becomes counter-clockwise and vice versa).
  • Rather than averaging the results of these rotations, the DFT simply sums them — no division by N is performed.

Otherwise, the process described above can be used to get the contribution of any single frequency within the signal, so then we just need to do this process for every possible frequency in the signal.

Thus, here we are, finally at some pseudocode to calculate the DFT:

void CalculateDFT(
  complex[] inputs,
  complex[] outputs,
  int length)
{
  for (outIdx = 0; outIdx < length; outIdx++)
  {
    // Start the sum at 0.
    float sum = 0

    // This is the angular frequency (in radians/sample) for
    // the given output sample (the "test frequency" from the
    // earlier examples).
    float angularFreq  = -2 * pi * outIdx / length

    for (inIdx = 0; inIdx < length; inIdx++)
    {
      // Get the rotation value for the current input index
      // then use that to calculate the resulting rotation.
      float radians = inIdx * angularFreq

      // `i` here is the imaginary unit
      complex rotation = cos(radians) + i*sin(radians)

      // A complex multiply with a unit complex vector is
      // a rotation.
      sum += inputs[inIdx] * rotation
    }

    outputs[outIdx] = sum;
  }
}
Note

As mentioned in a previous note, the standard formulation of the DFT uses an exponential shorthand (which I do not like) to represent the rotation. Thus, the DFT is written in standard mathematical notation as:

Xk=∑n=0N−1xn⋅e−i2πkNnX_k = \displaystyle\sum_{n = 0}^{N - 1} x_n \cdot e^{-i2\pi\frac{k}{N}n}

…which is equivalent to the above pseudocode.

We’ve finally done it! We have some code we can use to calculate the DFT for a given signal.

The above algorithm is O(N2)O(N^2), which may seem like the best that can be done. However, there’s a bit of algorithmic trickery that can be done to get that down to O(N log⁡ N)O(N\,\log\,N), which is considerably more efficient. That trickery is the fast Fourier transform (FFT), and the next post will go into the details of how that works!


(thanks to Neintonine, zerostride, and Chuck for giving me some great feedback on this before it went live!)