Learn › Numerical Computing
·FREE·PAPER·16 min read

The Fourier Transform from Scratch — Reading a Signal as a Stack of Pure Tones

A signal drawn against time hides the thing you usually want to know: which frequencies are in it, and how much of each. This article builds the Fourier transform from one idea — measure a signal against a pure tone with an inner product — then takes the continuous integral apart symbol by symbol, does the same for the DFT, separates two tones by hand, and ends on the three traps that bite in practice: aliasing, finite resolution, and spectral leakage.

Key points

  • A pure tone is fully described by three numbers, amplitude A, frequency f and phase φ, and can be written as a complex exponential e^{2πift}.
  • Over a window of length T, tones whose frequencies differ by k/T are orthogonal, so multiplying by a tone and averaging cancels every other frequency.
  • The Fourier transform X(f)=∫x(t)e^{-2πift}dt is the inner product of the signal with a probe tone at frequency f; |X(f)| is the amplitude and its angle is the phase.
  • In the DFT, bin index k corresponds to frequency f_k = k f_s / N, and the bin spacing Δf = f_s/N = 1/T depends only on recording length.
  • Sampling at or below 2 f_max causes aliasing, tones closer than Δf cannot be separated, and tones not on a bin centre leak across the spectrum.
ModalitytextTasknumerical

Théorie analytique de la chaleur

Primary source — what this article is built on

undefined2026-09-23

Théorie analytique de la chaleurFourier 1822Paper page
Discrete-Time Signal Processing"Oppenheim & Schafer 2010
https://www.pearson.com/en-us/subject-catalog/p/discrete-time-signal-processing/P200000003226"3rd ed.

The graph you have is not the graph you want

Record a second of a piano chord and plot it. What you get is a wobbling line: pressure against time. Somewhere in that wobble are three struck notes, the hum of the room, and the hiss of the microphone — but you cannot see any of them. The plot answers the question "what was the pressure at each instant?" when the question you actually have is "which tones are in here, and how loud is each one?"

The Fourier transform is the machine that converts the first answer into the second. It takes a function of time and returns a function of frequency, and every serious piece of signal work — audio codecs, JPEG, MRI reconstruction, the spectrogram a speech model eats for breakfast — starts by running it.

This article builds it in the order the idea actually assembles: the two prerequisites (the pure tone, and what it means for two tones to be orthogonal), the one measuring device that does all the work (the inner product), the continuous formula taken apart one symbol at a time, the discrete version you can actually compute, a worked example, and then the three things that go wrong.

Prerequisite 1: the pure tone

The atom of this whole subject is the sinusoid — a single pure tone:

x(t)=Asin⁡(2πft+ϕ)x(t) = A \sin(2\pi f t + \phi)

Three numbers describe it completely. AA is the amplitude, how loud. ff is the frequency in cycles per second (hertz), how high the pitch — 2πft2\pi f t turns cycles into radians, so that tt advancing by 1/f1/f seconds advances the angle by exactly one full turn. ϕ\phi is the phase, where in its cycle the wave starts at t=0t = 0.

In words: a pure tone is fully pinned down by three dials — how loud, how high, and where it starts.

Two conventions make the algebra bearable. First, write the tone as a complex exponential rather than a sine, using Euler's formula:

eiθ=cos⁡θ+isin⁡θe^{i\theta} = \cos\theta + i\sin\theta

Read as a picture: eiθe^{i\theta} is the point on the unit circle at angle θ\theta, with the cosine as its horizontal coordinate and the sine as its vertical one. So e2πifte^{2\pi i f t}, as tt runs forward, is a dot going around the unit circle ff times per second. That single object carries both the cosine and the sine at once, which is why it replaces them: one complex tone instead of two real ones.

Second, let amplitude and phase live together in one complex number. A complex number c=∣c∣eiϕc = |c| e^{i\phi} has a magnitude and an angle — exactly the two things a tone needs. So "the amount of frequency ff present" will come out as a single complex number whose magnitude is the amplitude and whose angle is the phase. That is not a trick; it is the reason the transform's output is complex-valued.

Prerequisite 2: orthogonality — why tones do not contaminate each other

Here is the fact that makes the whole method possible: different frequencies do not interfere with the measurement of each other. Stated precisely, over a window of length TT,

∫0Te2πif1t e2πif2t‾  dt=∫0Te2πi(f1−f2)t dt={Tf1=f20f1−f2=k/T, k≠0\int_0^T e^{2\pi i f_1 t}\,\overline{e^{2\pi i f_2 t}}\; dt = \int_0^T e^{2\pi i (f_1 - f_2) t}\, dt = \begin{cases} T & f_1 = f_2 \\ 0 & f_1 - f_2 = k/T,\ k \neq 0 \end{cases}
(1)

Term by term: the overbar is the complex conjugate (flip the sign of the imaginary part, which for eiθe^{i\theta} means e−iθe^{-i\theta}); multiplying e2πif1te^{2\pi i f_1 t} by e−2πif2te^{-2\pi i f_2 t} adds the exponents, leaving a single tone at the difference frequency f1−f2f_1 - f_2; and integrating a tone over a whole number of its own cycles gives zero, because every rise is cancelled by an equal fall. Only when the difference frequency is zero does the integrand become the constant 11, whose integral is the window length TT.

In words: multiply a signal by a tone, average, and every frequency except that one averages itself away. The frequencies fk=k/Tf_k = k/T — a whole number of cycles inside the window — form an orthogonal family, the continuous analogue of perpendicular axes. The Fourier transform is nothing more than reading off coordinates along those axes, and the "f1−f2=k/Tf_1 - f_2 = k/T" clause in the formula is the fine print that will come back to bite us later as leakage.

The intuition: a signal is a stack, and we weigh one layer at a time

Now the central claim, which is Fourier's 1822 insight: any reasonable signal can be written as a sum of pure tones. Not approximated by — written as. A square wave, a drum hit, a spoken vowel: each is some particular recipe of sinusoids added together. The signal is a stack of transparent layers, one per frequency, and what you see on the oscilloscope is the stack viewed from above.

If that is true, then "analysing" a signal means one thing: find the recipe. And thanks to orthogonality we can find it one ingredient at a time, without solving for all of them simultaneously. To ask "how much 440 Hz is in this signal?", multiply the signal by a 440 Hz tone and average. The 440 Hz layer, multiplied by itself, is everywhere non-negative and averages to something big. Every other layer, multiplied by the 440 Hz tone, becomes a tone at a nonzero difference frequency and averages to zero. One number falls out, and it is the amount of 440 Hz — regardless of what else was in there.

A geometric way to say the same thing: think of the signal as a vector in a space whose axes are frequencies. Multiplying-and-averaging is the inner product with one axis, i.e. a projection. Just as v⋅e^xv \cdot \hat{e}_x pulls the xx-coordinate out of a 3D vector without needing to know yy or zz, the inner product against e2πifte^{2\pi i f t} pulls out the ff-coordinate of a signal. The Fourier transform is the full list of those coordinates.

FIG 1The inner product measures how much of one vector points along the other. Turn them perpendicular (orthogonal) and it drops to zero — the same reason tones at different frequencies leave each other's measurement untouched.

The continuous Fourier transform, one symbol at a time

Take the inner-product recipe, let the window stretch to the whole time axis, and write it down:

X(f)=∫−∞∞x(t) e−2πift dtX(f) = \int_{-\infty}^{\infty} x(t)\, e^{-2\pi i f t}\, dt
(2)

Read left to right:

And the output, unpacked: ∣X(f)∣|X(f)| is how much of frequency ff is present (the amplitude), and arg⁡X(f)\arg X(f) is where in its cycle that component sits (the phase). Plotting ∣X(f)∣|X(f)| against ff gives the picture we wanted at the start — the chord's notes standing up as separate spikes.

The transform is reversible, which is what "the signal is the stack" really means:

x(t)=∫−∞∞X(f) e+2πift dfx(t) = \int_{-\infty}^{\infty} X(f)\, e^{+2\pi i f t}\, df
(3)

In words: to rebuild the signal, take every frequency's tone e2πifte^{2\pi i f t}, scale it by that frequency's coefficient X(f)X(f), and add them all up. Note the sign has flipped to ++ — analysis projects onto the conjugate, synthesis adds the tones themselves. Nothing is lost going either way: the time picture and the frequency picture are two descriptions of one object.

One immediate reading: at f=0f = 0 the probe is e0=1e^0 = 1, so X(0)=∫x(t) dtX(0) = \int x(t)\,dt — the DC component, the signal's total offset from zero. The lowest "frequency" is the average value.

The discrete version: what a computer actually runs

A computer has neither an infinite time axis nor a continuum of samples. It has NN numbers x0,x1,…,xN−1x_0, x_1, \dots, x_{N-1}, taken at a sampling rate fsf_s samples per second, so sample nn sits at time t=n/fst = n/f_s and the whole record lasts T=N/fsT = N/f_s seconds. Replace the integral by a sum and the continuum of frequencies by the orthogonal family fk=k/Tf_k = k/T, and the formula becomes the Discrete Fourier Transform (DFT):

Xk=∑n=0N−1xn e−2πikn/N,k=0,1,…,N−1X_k = \sum_{n=0}^{N-1} x_n\, e^{-2\pi i k n / N}, \qquad k = 0, 1, \dots, N-1
(4)

In words: for each of NN test frequencies, multiply the samples by that frequency's tone and add them up; the total is how much of that frequency the record contains.

Every piece maps onto something in the continuous version:

That last line is the single most useful fact in applied Fourier work: bin index kk converts to hertz as fk=kfs/Nf_k = k f_s / N, and consecutive bins are spaced by

Δf=fsN=1T\Delta f = \frac{f_s}{N} = \frac{1}{T}

In words: the finest frequency detail you can see is one over the length of the recording.

The spacing depends only on how long you recorded, not how fast you sampled. Want to tell 440 Hz from 441 Hz? You need Δf≤1\Delta f \le 1 Hz, so you need at least one full second of audio. No amount of oversampling substitutes for it.

The inverse is the same sum with the sign flipped and a 1/N1/N out front:

xn=1N∑k=0N−1Xk e+2πikn/Nx_n = \frac{1}{N} \sum_{k=0}^{N-1} X_k\, e^{+2\pi i k n / N}

In words: add every bin's tone back together, each scaled by its coefficient, and you get the original samples exactly.

The 1/N1/N is there because the discrete orthogonality relation is ∑n=0N−1e2πi(k−k′)n/N=N\sum_{n=0}^{N-1} e^{2\pi i (k - k')n/N} = N when k=k′k = k' (and 00 otherwise — it is a geometric series whose ratio is an NN-th root of unity). Projecting onto a basis vector of squared length NN overcounts by NN, and the inverse divides it back out. Where that constant sits is pure convention, which is why different libraries normalise differently — a real source of factor-of-NN bugs.

Computed as written, the DFT costs NN multiplications for each of NN bins: O(N2)O(N^2). The FFT is the divide-and-conquer reorganisation that gets the same numbers in O(Nlog⁡N)O(N \log N); it is a faster route to this formula, not a different transform. That algorithm is the subject of The FFT from Scratch.

Worked example: pulling two tones back apart

Mix two sinusoids, one loud and low, one quiet and higher:

x(t)=sin⁡(2π⋅5t)+0.5sin⁡(2π⋅12t)x(t) = \sin(2\pi \cdot 5t) + 0.5 \sin(2\pi \cdot 12t)

Sample it at fs=128f_s = 128 Hz for exactly one second, so N=128N = 128 and Δf=128/128=1\Delta f = 128/128 = 1 Hz. Each bin is one hertz wide, and both tones sit on exact bin centres — 5 Hz lands in bin 5, 12 Hz in bin 12.

import numpy as np

fs, N = 128, 128
t = np.arange(N) / fs
x = np.sin(2*np.pi*5*t) + 0.5*np.sin(2*np.pi*12*t)

X = np.fft.fft(x)
mag = np.abs(X)

for k in np.argsort(mag)[::-1][:4]:
    print(f"bin {k:3d}  =  {k*fs/N:5.1f} Hz   |X| = {mag[k]:.1f}")

Out come exactly four peaks, and every one of them is predictable:

bin   5  =    5.0 Hz   |X| = 64.0
bin 123  =  123.0 Hz   |X| = 64.0
bin  12  =   12.0 Hz   |X| = 32.0
bin 116  =  116.0 Hz   |X| = 32.0

Three things to read off. First, the magnitudes. A real sinusoid of amplitude AA produces a peak of height NA/2NA/2, because sin⁡θ=(eiθ−e−iθ)/2i\sin\theta = (e^{i\theta} - e^{-i\theta})/2i splits the tone into two complex exponentials of size A/2A/2, and the sum over NN samples multiplies each by NN. So A=1A = 1 gives 128×1/2=64128 \times 1/2 = 64 and A=0.5A = 0.5 gives 3232. The ratio of the peaks is the ratio of the amplitudes — the transform recovered the recipe.

Second, the mirror. Bins 123 and 116 are the e−iθe^{-i\theta} halves: 123=128−5123 = 128 - 5 and 116=128−12116 = 128 - 12. For any real-valued input, XN−k=Xk‾X_{N-k} = \overline{X_k}, so the top half of the spectrum is a reflection of the bottom half and carries no new information. This is why plots stop at fs/2f_s/2 and why np.fft.rfft exists.

Third, the phase. Printing the raw values gives X5=−64iX_5 = -64i and X12=−32iX_{12} = -32i: purely imaginary, no real part. That is the signature of a sine rather than a cosine — a cosine would have come out purely real. Discarding the phase and keeping only ∣X∣|X| throws that away, which is exactly why you cannot reconstruct audio from a magnitude spectrogram alone without some phase-recovery trick.

Three pitfalls, in the order they will bite you

Aliasing: sampling too slowly forges a frequency

The Nyquist–Shannon sampling theorem says a signal containing no frequency above fmax⁡f_{\max} is fully determined by samples taken at fs>2fmax⁡f_s > 2 f_{\max}. Half the sampling rate, fs/2f_s/2, is the Nyquist frequency — the highest frequency the samples can represent at all.

Break that and the energy does not vanish; it comes back in disguise. A tone at ff above Nyquist is indistinguishable, at the sample instants, from one at ∣f−mfs∣|f - m f_s| for the integer mm that brings it into range. Add np.sin(2*np.pi*100*t) to the example above, still at fs=128f_s = 128: 100 Hz is past Nyquist (64 Hz), and the peak appears at bin 28 — because 128−100=28128 - 100 = 28. A perfectly clean 100 Hz tone is reported as a perfectly clean 28 Hz tone, and nothing in the data marks it as fake. The only fix is prevention: a low-pass anti-aliasing filter before the sampler, which is why every audio interface has one in hardware.

Resolution: a short window cannot tell close frequencies apart

Δf=1/T\Delta f = 1/T is a hard floor. Two tones closer together than one bin land in the same bin and merge into a single peak; no post-processing separates them. The standard mistake is to zero-pad the record and celebrate the smoother-looking spectrum: zero-padding interpolates the same underlying information onto a finer grid, making peaks easier to locate, but it does not resolve tones that were already merged. And since TT is also the interval over which the spectrum is an average, wanting both fine frequency detail and fine timing detail puts you in direct conflict — the time–frequency trade-off that forces a spectrogram to pick a window length and live with it.

Leakage: a tone that is not an exact bin smears across the whole spectrum

Go back to the orthogonality condition: it held only for frequency differences that are whole numbers of cycles per window. A tone that completes a fractional number of cycles inside the window is orthogonal to nothing, and its energy spills into every bin. Take sin⁡(2π⋅5.5t)\sin(2\pi \cdot 5.5 t) with the same N=128N = 128 — 5.5 cycles in the window, dead between bins:

bin:     3      4      5      6      7      8
|X|:  10.5   15.7   42.6   39.0   12.0    6.7

Instead of one spike of 64 there are two shoulders of ~41 and a long tail of skirts. Physically, the DFT assumes the record repeats forever, so cutting a wave mid-cycle creates a discontinuity at the seam, and that artificial jump has broadband content.

The standard treatment is a window function: multiply the samples by a curve that tapers smoothly to zero at both ends, removing the seam. Applying a Hann window to the same signal gives

bin:     3      4      5      6      7      8
|X|:  0.81   5.60  27.01  27.03   5.58   0.79

The distant skirts drop by more than an order of magnitude. Note what it cost: the main lobe is still spread over two bins (slightly wider than before), and the peak heights shrank because the window removed signal energy. That is the permanent bargain of windowing — lower sidelobes in exchange for a wider main lobe and an amplitude correction you have to apply yourself.

How this shows up on the job

You will essentially never write the DFT sum by hand; you will call np.fft.rfft, torch.stft, or librosa.melspectrogram. What you will spend real time on is the four parameters those functions expose, and each one is a pitfall above wearing a keyword argument.

n_fft is your frequency resolution. It fixes Δf=fs/nfft\Delta f = f_s / n_{\mathrm{fft}}. At 16 kHz speech with n_fft=512, bins are 31 Hz apart — fine for formants, too coarse to separate the harmonics of a low male voice. Raising it buys frequency detail and pays in time detail.

hop_length and window are leakage management. A spectrogram chops audio into overlapping frames; each frame is a finite window, so each frame leaks. Hann is the default for a reason, and the conventional 50–75 % overlap exists so that the tapered-away edges of one frame are covered by the middle of the next.

Normalisation conventions differ, and the symptom is a clean factor of NN. NumPy puts the 1/N1/N on the inverse; some libraries use 1/N1/\sqrt{N} on both sides; rfft returns N/2+1N/2 + 1 bins, so summing its magnitudes double-counts nothing but summing power without a factor of 2 for the folded half undercounts. When a measured level is off by a suspiciously round factor, check the convention before the physics.

Anything reported below Nyquist can still be an alias. If a spectrum shows a tone you cannot account for, verify the anti-aliasing path before hunting for it in the source. Resampling in software has the same requirement: downsampling without low-passing first folds the discarded top octave back down into the audible band.

Summary

For the O(Nlog⁡N)O(N \log N) algorithm and the convolution theorem that follows from it, continue to The FFT from Scratch. For the linear-algebra reading of "project onto an orthogonal basis", see Linear Algebra for AI.

Exercises

  1. You record 0.25 seconds of audio at fs=8000f_s = 8000 Hz. What is the bin spacing Δf\Delta f, how many bins does the DFT produce, and which bin index corresponds to 1000 Hz?
  2. A signal is sampled at fs=1000f_s = 1000 Hz and contains a pure tone at 700 Hz with no anti-aliasing filter. At what frequency will the peak appear, and why can you not detect from the samples alone that it is spurious?
  3. A colleague needs to distinguish two engine harmonics 3 Hz apart. He records 0.2 s at a very high sampling rate and zero-pads to 65536 points, then reports that the peaks still merge. What is wrong with his approach, and what must he change?
  4. Taking the DFT of cos⁡(2π⋅10t)\cos(2\pi \cdot 10 t) with fs=64f_s = 64, N=64N = 64 gives X10=32X_{10} = 32 — purely real — while the same experiment with sin⁡\sin gives X10=−32iX_{10} = -32i. Explain both the magnitude 32 and the difference in phase.
Answers
  1. Δf=1/T=1/0.25=4\Delta f = 1/T = 1/0.25 = 4 Hz. The record holds N=fsT=8000×0.25=2000N = f_s T = 8000 \times 0.25 = 2000 samples, so the DFT produces 2000 bins (of which 1001 are independent for a real signal). 1000 Hz sits at k=f/Δf=1000/4=250k = f/\Delta f = 1000/4 = 250.
  2. Nyquist is fs/2=500f_s/2 = 500 Hz, and 700 Hz is above it, so the tone folds to ∣700−1000∣=300|700 - 1000| = 300 Hz. The samples of a 700 Hz tone and of a 300 Hz tone are numerically identical at these instants, so no analysis of the sample values can tell them apart — the information was destroyed at sampling time, not lost in processing. Only a filter before the sampler prevents it.
  3. Resolution is fixed by record length: Δf=1/0.2=5\Delta f = 1/0.2 = 5 Hz, which is coarser than the 3 Hz gap, so the two tones fall in one bin. Sampling rate raises the highest representable frequency, not the resolution, and zero-padding only interpolates the spectrum onto a finer grid — it cannot recover a distinction the window never captured. He must record longer: at least T>1/3≈0.34T > 1/3 \approx 0.34 s, and in practice a few times that for a clean separation.
  4. Magnitude: a real sinusoid of amplitude AA splits into two complex exponentials of size A/2A/2, each summing to NA/2N A/2 at its bin — here 64×1/2=3264 \times 1/2 = 32. Phase: cos⁡θ=(eiθ+e−iθ)/2\cos\theta = (e^{i\theta} + e^{-i\theta})/2 gives two positive halves, so the coefficient is real, whereas sin⁡θ=(eiθ−e−iθ)/2i\sin\theta = (e^{i\theta} - e^{-i\theta})/2i carries the 1/i=−i1/i = -i factor, rotating the coefficient onto the negative imaginary axis. The magnitude is the same because amplitude is the same; the argument differs by 90° because that is precisely the phase offset between a sine and a cosine.
🎬 Watch this explained (Manim, captioned)

References

  1. Théorie analytique de la chaleur. Fourier 1822Paper page
  2. Discrete-Time Signal Processing. "Oppenheim & Schafer 2010
  3. https://www.pearson.com/en-us/subject-catalog/p/discrete-time-signal-processing/P200000003226". 3rd ed.

This article is written from the source paper above. Where they differ, the original is authoritative.

Published by TechAthletes · Editorial policy · How we cite · Corrections

Comments

Sign in to comment