Analytic Signal Lab — 3 lessons (same on every page in this series)
1. Hilbert / FFT (you are here) → 2. Envelope & instantaneous frequency → 3. Phase demodulation
Tools: Hilbert analytic signal demo · FFT window functions demo
In one sentence: The analytic signal $latex z(t)=x(t)+j\hat{x}(t)$ is the real signal $latex x(t)$ plus its Hilbert transform as the imaginary part; it has no negative frequencies, so its magnitude is the envelope, its angle is the instantaneous phase, and the derivative of that angle is the instantaneous frequency.
Key focus of this article: Define the analytic signal rigorously, state when the envelope really is the envelope (Bedrosian’s theorem), construct it exactly with an FFT, and learn where the construction fails. Python and MATLAB code is included, and every curve can be reproduced in the interactive demo below.
Why the real signal is redundant
Let $latex x(t)$ be a real, finite-energy signal with Fourier transform $latex X(f)=\int x(t)e^{-j2\pi ft}\,dt$. Because $latex x(t)$ is real, $latex X(-f)=X^{*}(f)$: the negative-frequency half of the spectrum is the complex conjugate of the positive half. It carries no extra information. A real sinusoid $latex \cos(2\pi f_0 t)$ is the sum of two counter-rotating phasors $latex \tfrac12 e^{j2\pi f_0 t}+\tfrac12 e^{-j2\pi f_0 t}$, and if we keep only one of them (and double it), we get a single rotating phasor whose length and angle are directly readable.
That is the idea behind the analytic signal, introduced by Gabor [1] and Ville [2]: keep the positive-frequency content of $latex x(t)$, discard the redundant negative half, and take the resulting complex signal as the natural complex companion of the real waveform. Practical payoffs include envelope detection, instantaneous frequency estimation, single-sideband generation and bias-free spectral estimation [3].
Definition: analytic signal and Hilbert transform
The Hilbert transform of $latex x(t)$ is the convolution with $latex 1/(\pi t)$ (principal value):
$latex \displaystyle \hat{x}(t)=\mathcal{H}\{x(t)\}=\frac{1}{\pi}\,\mathrm{p.v.}\!\int_{-\infty}^{\infty}\frac{x(\tau)}{t-\tau}\,d\tau \quad (1)$
In the frequency domain this is a pure phase filter with unit magnitude: $latex \hat{X}(f)=H(f)X(f)$ with
$latex \displaystyle H(f)=-j\,\operatorname{sgn}(f) \quad (2)$
Positive frequencies are rotated by $latex -90^\circ$, negative frequencies by $latex +90^\circ$. Hence $latex \mathcal{H}\{\cos(2\pi f_0t)\}=\sin(2\pi f_0t)$, $latex \mathcal{H}\{\sin(2\pi f_0t)\}=-\cos(2\pi f_0t)$, and applying the transform twice gives $latex -x(t)$ (except for any DC term, which is annihilated). The analytic signal is
$latex \displaystyle z(t)=x(t)+j\hat{x}(t) \quad (3)$
Take the Fourier transform of (3) and use (2):
$latex \displaystyle Z(f)=X(f)+j\bigl(-j\operatorname{sgn}f\bigr)X(f)=X(f)\bigl[1+\operatorname{sgn}(f)\bigr]=2X(f)\,u(f) \quad (4)$
where $latex u(f)$ is the unit step (with $latex Z(0)=X(0)$ by convention). Equation (4) is the whole story: the analytic signal is the real signal with its negative frequencies removed and its positive frequencies doubled. Adding $latex j\hat{x}$ to $latex x$ cancels the negative-frequency half of the spectrum and reinforces the positive half. This is the same operation as in Figure 1.

Properties worth memorising
- Real part is the signal: $latex \operatorname{Re}\{z(t)\}=x(t)$, so nothing is lost. Recover $latex x$ from $latex z$ at any time.
- Orthogonality: $latex \int x(t)\hat{x}(t)\,dt=0$. A signal and its Hilbert transform are orthogonal (they are in quadrature).
- Equal energy: $latex \int \hat{x}^2\,dt=\int x^2\,dt$ for signals without DC, because $latex |H(f)|=1$. The energy of $latex z$ is exactly twice that of $latex x$.
- Complex exponential: $latex x=\cos(2\pi f_0t)$ gives $latex z=e^{j2\pi f_0t}$ for $latex f_0>0$: constant envelope, linear phase, frequency $latex f_0$.
- Polar form: $latex z(t)=a(t)e^{j\varphi(t)}$ with $latex a=|z|$, $latex \varphi=\arg z$.
Envelope, instantaneous phase and instantaneous frequency
Writing the analytic signal in polar form defines three time-varying quantities:
$latex \displaystyle a(t)=\sqrt{x^2(t)+\hat{x}^2(t)},\qquad \varphi(t)=\arctan\frac{\hat{x}(t)}{x(t)},\qquad f_i(t)=\frac{1}{2\pi}\frac{d\varphi(t)}{dt} \quad (5)$
The first is the instantaneous amplitude or envelope, the second the instantaneous phase (which must be unwrapped, otherwise it saws between $latex -\pi$ and $latex \pi$), the third the instantaneous frequency. For a sampled signal at rate $latex f_s$, approximate the derivative with a phase difference, $latex f_i[n]\approx \frac{f_s}{2\pi}\bigl(\varphi[n+1]-\varphi[n]\bigr)$. You can avoid unwrapping by using $latex f_i[n]=\frac{f_s}{2\pi}\arg\{z[n+1]z^{*}[n]\}$, which returns the wrapped phase increment directly and is valid whenever the true increment is below $latex \pi$ per sample.
A caution before using (5): the decomposition $latex x(t)=a(t)\cos\varphi(t)$ is not unique — infinitely many pairs $latex (a,\varphi)$ reproduce the same real waveform. The analytic signal is one particular choice, and it is only physically meaningful when the signal is narrowband and mono-component [5]. The next section makes this precise.
When is the envelope really the envelope? Bedrosian’s theorem
Consider an AM waveform $latex x(t)=a(t)\cos(2\pi f_ct)$. We want to claim that $latex z(t)=a(t)e^{j2\pi f_ct}$, so that $latex |z|=|a|$ and the Hilbert transform is $latex \hat{x}=a(t)\sin(2\pi f_ct)$. That is true under a precise condition.
Bedrosian’s theorem [4]: if $latex a(t)$ is low-pass with $latex A(f)=0$ for $latex |f|>B$, and $latex c(t)$ is high-pass with $latex C(f)=0$ for $latex |f|<B$, then
$latex \displaystyle \mathcal{H}\{a(t)c(t)\}=a(t)\,\mathcal{H}\{c(t)\} \quad (6)$
Sketch of proof. The spectrum of the product is the convolution $latex (A*C)(f)=\int A(\nu)C(f-\nu)\,d\nu$. For the integrand to be non-zero we need $latex |\nu|<B$ and $latex |f-\nu|>B$. Then $latex f-\nu$ has the same sign as $latex f$ for every $latex \nu$ that contributes. So $latex \operatorname{sgn}(f)=\operatorname{sgn}(f-\nu)$ inside the integral and $latex \operatorname{sgn}(f)\,(A*C)(f)=A*(\operatorname{sgn}\cdot C)$. Multiplying by $latex -j$ gives (6). ■
Apply it with $latex c(t)=\cos(2\pi f_ct)$. If the message bandwidth satisfies $latex B<f_c$, then $latex \mathcal{H}\{a\cos(2\pi f_ct)\}=a\sin(2\pi f_ct)$ and $latex z=a\,e^{j2\pi f_ct}$. Equivalently, the spectrum of $latex a(t)e^{j2\pi f_ct}$ is $latex A(f-f_c)$, which already lives entirely at positive frequencies, so it is its own analytic signal. The envelope detector $latex |z|$ then returns $latex |a(t)|$, and the instantaneous frequency is exactly $latex f_c$. If $latex B\ge f_c$ the shifted spectrum straddles DC, the sideband energy at negative frequencies is thrown away by (4), and the envelope and frequency estimates distort. The demo shows this when you drag the carrier toward 30 Hz with a deep modulation.
Constructing the analytic signal with the FFT
For a sampled record $latex x[n]$, $latex n=0,\dots,N-1$, we cannot simply zero “negative frequencies” of the DTFT because the spectrum of a sampled signal is periodic: wiping out one period wipes out everything. The standard remedy [6] is to work on the DFT grid and treat bins $latex N/2+1,\dots,N-1$ (the upper half of the DFT, which represent negative frequencies) as the negative half. Compute
$latex \displaystyle X[k]=\sum_{n=0}^{N-1}x[n]e^{-j2\pi kn/N},\qquad Z[k]=h[k]\,X[k],\qquad z[n]=\frac{1}{N}\sum_{k=0}^{N-1}Z[k]e^{j2\pi kn/N} \quad (7)$
with the weights
- $latex N$ even: $latex h[0]=h[N/2]=1$; $latex h[k]=2$ for $latex 1\le k\le N/2-1$; $latex h[k]=0$ for $latex N/2+1\le k\le N-1$.
- $latex N$ odd: $latex h[0]=1$; $latex h[k]=2$ for $latex 1\le k\le (N-1)/2$; $latex h[k]=0$ otherwise.
![(a) CTFT of continuous signal x(t), (b) periodic spectrum of x[n] after sampling and (c) one-sided periodic spectrum of the analytic signal z[n]](https://www.gaussianwaves.com/gaussianwaves/wp-content/uploads/2017/04/discrete_time_analytic_signal_PNG.png)
DC ($latex k=0$) and, for even $latex N$, the Nyquist bin ($latex k=N/2$) are their own mirror images, so they are kept once rather than doubled. This is precisely the one-sided construction in (4): $latex Z[k]=X[k]\bigl(1+s[k]\bigr)$ with $latex s[k]=+1$ on the positive bins, $latex -1$ on the negative bins and $latex 0$ at DC and Nyquist. Equivalently, the discrete Hilbert transform is $latex \hat{x}[n]=\mathrm{IDFT}\{-j\,s[k]X[k]\}$ and $latex z=x+j\hat{x}$.
Because everything happens on the DFT grid, three exact properties follow immediately from Parseval:
- $latex \operatorname{Re}\{z[n]\}=x[n]$ for every $latex n$.
- $latex \sum_n x[n]\hat{x}[n]=0$ exactly, since $latex |X[k]|=|X[N-k]|$ and the weight $latex -js[k]$ is odd in $latex k$.
- For even $latex N$: $latex \sum_n\hat{x}^2[n]=\sum_n x^2[n]-\bigl(X^2[0]+X^2[N/2]\bigr)/N$. The deficit is the DC and Nyquist energy, which the Hilbert transform cannot represent. The demo shows this ratio as the “Hilbert energy check”.
In the time domain, the same operation is a circular convolution with the impulse response $latex \hat{h}[n]=\frac{2}{N}\cot(\pi n/N)$ for odd $latex n$ and $latex 0$ for even $latex n$ (even $latex N$). Note the slow $latex 1/n$ decay: every sample of the output depends on the entire record, which is why end effects (below) are unavoidable.
Python and MATLAB
The function below is exactly what scipy.signal.hilbert and MATLAB’s hilbert do. Both libraries return the analytic signal $latex z$, not $latex \hat{x}$. To get the Hilbert transform, take the imaginary part.
import numpy as np
def analytic_signal(x):
"""Analytic signal via the FFT (same weights as scipy.signal.hilbert)."""
x = np.asarray(x, dtype=float)
N = x.size
X = np.fft.fft(x)
h = np.zeros(N)
h[0] = 1
if N % 2 == 0:
h[N // 2] = 1
h[1:N // 2] = 2
else:
h[1:(N + 1) // 2] = 2
return np.fft.ifft(X * h)
fs, N = 1000.0, 1024
t = np.arange(N) / fs
m, fm, fc = 0.6, 8.0, 120.0
x = (1 + m * np.cos(2 * np.pi * fm * t)) * np.cos(2 * np.pi * fc * t)
z = analytic_signal(x)
env = np.abs(z) # envelope
phase = np.unwrap(np.angle(z)) # instantaneous phase
f_inst = np.diff(phase) * fs / (2 * np.pi) # instantaneous frequency (Hz)
f_inst2 = np.angle(z[1:] * np.conj(z[:-1])) * fs / (2 * np.pi) # no unwrap needed
trim = int(0.05 * N) # ignore the ends (see pitfalls)
print(env[trim:-trim].mean(), f_inst[trim:-trim].mean())
fs = 1000; N = 1024; t = (0:N-1)/fs;
x = (1 + 0.6*cos(2*pi*8*t)) .* cos(2*pi*120*t);
z = hilbert(x); % analytic signal, NOT just the Hilbert transform
xh = imag(z); % Hilbert transform of x
env = abs(z);
phase = unwrap(angle(z));
finst = diff(phase) * fs / (2*pi);
% The same thing from first principles
X = fft(x); h = zeros(1, N);
if mod(N, 2) == 0
h([1 N/2+1]) = 1; h(2:N/2) = 2;
else
h(1) = 1; h(2:(N+1)/2) = 2;
end
z2 = ifft(X .* h);
max(abs(z - z2)) % ~1e-15

For this AM signal (carrier 120 Hz, 8 Hz message, $latex m=0.6$) the mean envelope over the interior of the record is close to 1, the instantaneous frequency is a flat line at 120 Hz, and the Hilbert energy ratio is 1 to four decimals. You can reproduce those numbers, and change every parameter, in the interactive demo.
Worked example: why two tones are not one carrier
Let $latex x(t)=\cos(2\pi f_1t)+m\cos(2\pi f_2t)$ with $latex f_2=f_1+\Delta$. Then
$latex \displaystyle z(t)=e^{j2\pi f_1t}\bigl(1+me^{j2\pi\Delta t}\bigr),\quad a(t)=\sqrt{1+m^2+2m\cos 2\pi\Delta t},\quad f_i(t)=f_1+\Delta\,\frac{m^2+m\cos 2\pi\Delta t}{1+m^2+2m\cos 2\pi\Delta t}$
The envelope beats at $latex \Delta$, and the instantaneous frequency oscillates around the frequency of the stronger tone (the time average is $latex f_1$ for $latex m<1$ and $latex f_2$ for $latex m>1$). For $latex m$ close to 1 it swings violently, and the minimum can lie outside $latex [f_1,f_2]$. Nothing is wrong with the transform: the signal has two components, and “the” instantaneous frequency of a bi-component signal is not a single physical frequency [5]. In practice, separate components first (band-pass filter, filter bank, EMD) and apply the Hilbert transform to each one.
Pitfalls
- End effects and leakage. The FFT treats the record as periodic. If the record is not an integer number of cycles, the wrap-around discontinuity leaks energy across bins and the discrete Hilbert transform (with its $latex 1/n$ kernel) rings at both ends. Envelope and frequency estimates are least reliable in the first and last few percent of samples. Discard them, window the data, or use longer records. See FFT and spectral leakage.
- Non-narrowband signals. If the envelope spectrum reaches or crosses the carrier, Bedrosian (6) fails: $latex |z|\neq a$ and $latex f_i\neq f_c$. Over-modulation ($latex m>1$) is a special case: the “envelope” $latex a(t)$ changes sign, but $latex |z|=|a|$ cannot, so the phase jumps by $latex \pi$.
- Multi-component signals. Speech, vibration, chirps overlapping in time, or any sum of tones have no single instantaneous frequency. Filter first.
- Noise. The envelope is robust, but frequency is a derivative. White noise adds phase jitter of roughly $latex 1/\sqrt{2\,\mathrm{SNR}}$ radians at high SNR, and differentiating it multiplies it by $latex f_s/2\pi$. Smooth the frequency estimate or lower the sampling rate before estimating. At low SNR the phase can slip by $latex 2\pi$ and unwrapping breaks.
- DC and Nyquist. Both are dropped by the imaginary part. Remove the mean explicitly if it should not affect the envelope.
- Bandwidth of the envelope. $latex |z|^2$ has twice the bandwidth of $latex z$, so sample a few times above the Nyquist rate of the message if you plan to process $latex a[n]$ nonlinearly.
- Real-time use. The ideal Hilbert transformer is non-causal with infinite support. A practical FIR approximation (Type III or Type IV linear-phase, designed by Parks–McClellan or windowed truncation) has a delay of $latex (L-1)/2$ samples that must also be applied to the real part before forming $latex z$. The window choice sets the lowest and highest usable frequency; see window function figures of merit.
Exercises with the lab
- Open the clean AM tone. Confirm the envelope equals $latex 1+0.6\cos(2\pi\cdot 8t)$ and the frequency is flat at 120 Hz.
- Lower the SNR to 10 dB. The envelope degrades gently; the instantaneous frequency does not.
- Switch to chirp. Unit envelope, parabolic phase, straight frequency ramp.
- Try the two-tone case and compare the frequency curve with the formula above.
- Set N = 256 and move the carrier by a few Hz. Watch the end-effect error change as the number of cycles in the record becomes non-integer.
FAQ
What is the difference between the Hilbert transform and the analytic signal? The Hilbert transform $latex \hat{x}$ is a real signal, a $latex 90^\circ$ phase-shifted version of $latex x$. The analytic signal $latex z=x+j\hat{x}$ is a complex signal built from both. MATLAB’s and SciPy’s hilbert return $latex z$; take imag to get $latex \hat{x}$.
Why double the positive frequencies? The negative-frequency half carries the same energy as the positive half. Dropping it would halve the amplitude, so the positive half is scaled by 2 to keep $latex \operatorname{Re}\{z\}=x$. Equivalently, $latex \cos\omega t=\tfrac12(e^{j\omega t}+e^{-j\omega t})$ becomes $latex e^{j\omega t}$, which has unit length rather than one half.
Why are DC and Nyquist not doubled? They map onto themselves under $latex k\to N-k$, so there is no mirror image to absorb. Doubling them would break $latex \operatorname{Re}\{z\}=x$.
Can the envelope be wrong even when |z| looks smooth? Yes. Whenever Bedrosian’s condition fails, $latex |z|$ is a perfectly valid mathematical object but it is no longer the AM message. Check that the message bandwidth is clearly below the carrier frequency, and inspect the spectrum of $latex x$ before trusting an envelope.
Why is the instantaneous frequency so noisy? It is the derivative of the phase. Noise adds a small random phase error at each sample, and differencing adjacent samples amplifies it by $latex f_s/2\pi$. Average over several samples (the demo uses 5), use a longer baseline, or fit the phase locally with a polynomial.
Can I use the Hilbert transform on a real-time stream? Yes, with a linear-phase FIR Hilbert transformer and a matching delay on the real branch. The accuracy at low and high frequencies depends on the filter length and window.
References
[1] D. Gabor, “Theory of communication,” J. Inst. Electr. Eng., vol. 93, pt. III, pp. 429–457, 1946.
[2] J. Ville, “Théorie et applications de la notion de signal analytique,” Câbles et Transmission, vol. 2A, pp. 61–74, 1948.
[3] S. M. Kay, “Maximum entropy spectral estimation using the analytical signal,” IEEE Trans. Acoust., Speech, Signal Process., vol. 26, no. 5, pp. 467–469, Oct. 1978.
[4] E. Bedrosian, “A product theorem for Hilbert transforms,” Proc. IEEE, vol. 51, no. 5, pp. 868–869, May 1963.
[5] B. Boashash, “Estimating and interpreting the instantaneous frequency of a signal,” Proc. IEEE, vol. 80, no. 4, pp. 520–538, Apr. 1992 (Part 1).
[6] S. L. Marple Jr., “Computing the discrete-time analytic signal via FFT,” IEEE Trans. Signal Process., vol. 47, no. 9, pp. 2600–2603, Sept. 1999.
[7] F. R. Kschischang, “The Hilbert Transform,” University of Toronto, Oct. 2006.
Similar articles
- Extract envelope, instantaneous phase and frequency using the Hilbert transform
- Phase demodulation using the Hilbert transform
- Interpreting FFT results: magnitude and phase
- FFT and spectral leakage
- Hilbert analytic signal demo (interactive)
Hello, thank you for sharing this. I am having a problem with the instantaneous phase I am getting out of the hilbert(.) function in MATLAB, as well as when I do not use fft/ifft to get the analytical phase. So the general question is, does my phase get distorted when I use Hilbert Transform on a random signal?
More elaborate example on this is, when I generate a random signal of length T, and then keep this random signal fixed, then I generate, say, few more random signal of length S, and then concatenate it with my original signal of length T, I do see that there is slight phase distortion at the beginning of my signal. In other words, why concatenating some signal to the end of my original signal distorts the instantaneous phase, in particular, at the beginning?
Many people writing CODE for signal processing use FFTs and HILBERT Transforms.
I have read descriptions of the HILBERT Transform. And I think that a little less math (and more words about how to operate on the complex numbers of the FFT bin locations , would be more useful. As an example, a HILBERT transform can be implemented by : taking the FFT of a timedomain signal, visit every bin of the FFT array, (set BIN 0] to ZERO. Then, visit each BIN , one at a time. swap the REALP value with the IMAGP (and then multiply the REALP by -1). Do this at each BIN. Then TAKE the INVERSE FFT of the modified frequency domain array to get the 90-degree phase shifted signal. Or take the FFT of a timedomain signal, (set BIN 0] to ZERO. Then, visit each BIN , one at a time, from BIN(N/2 +1) to BIN (N) and set the REALP to ZERO and the IMAGP to zero. Then TAKE the INVERSE FFT of the modified frequency domain array to get the 90-degree phase shifted signal. It is difficult to progress from integrals to what to do (in C or PYTHON) with the AMPLITUDE SAMPLES of a timedomain signal or what to do with the REALP and IMAGP of every complex number of the FFT BIN. It would be nice if the authors wrote the INTEGRALS and some pseudo-code. Thank you. Cesar in California
Hi. Thank you so much for this interesting post. I just have a question about the python script.
It seems you made fft on a real value signal and got a complex vector as a result. Then at the end you reversed it by ifft. I expected to get again a real value vector, but surprisingly the output was complex too. I guess the zeros you added at the bottom of “h” caused it, but how? can you please explain a bit about how we got a complex vector at the output?
Thanks a million!
Please read and grasp the concept. The aim is not to demonstrate FFT/IFFT. The aim is to create an analytic signal (complex vector) from a real signal (real vector).